Compare commits

..

144 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
b64084d248
[Docs] Update changelog and install details 2022-07-08 09:04:57 +01:00
Calum Lind
e120536d87
Fix parsing magnet with tracker tiers
Magnets with trackers specified with tr.x param were not being unquoted
so unusable raw tracker string was being set.

Fixed by unquoting tracker and adding test

See-also: https://dev.deluge-torrent.org/ticket/2716
2022-07-08 08:34:29 +01:00
Calum Lind
f52cf760e4
Fix missing trackers adding magnets
The changes to remove deprecated lt methods didn't account for magnet
trackers so magnets are missing trackers when added.

Previously the addition of trackers was handled by libtorrent when a url
was passed in add_torrent_params. The url parameter is deprecated so
instead we need to add both the info_hash and trackers.

Trac: https://dev.deluge-torrent.org/ticket/3530
2022-07-05 08:03:33 +01:00
Calum Lind
94d790c159
[CI] Bump ifaddr to 0.2.0
With release of ifaddr 0.2.0 no longer need to pin to github commit to
resolve Windows decoding issues.
2022-06-30 21:47:06 +01:00
Calum Lind
f78506161d
[CI] Fix failing Windows Python 3.10 tests
A recent dependency change caused the tests running on GitHub Actions
under Python 3.10.5 on Windows to fail when starting pytest run:

    ...
    INTERNALERROR>   File "<frozen importlib._bootstrap>", line 123, in acquire
    INTERNALERROR> KeyError: xxxx

The cause seems to have been a newer version of chardet package released
recently.

* Fixed by pinning chardet to v4
* Also pin Windows version to 2019 to match packaging workflow

See-also: https://github.com/deluge-torrent/deluge/actions/runs/2578427588
Issue: https://github.com/chardet/chardet/issues/265
2022-06-29 15:07:23 +01:00
Calum Lind
592b05cd87
back to development 2022-06-28 22:11:29 +01:00
Calum Lind
6c8f9ce756
Release 2.1.0 2022-06-28 22:07:35 +01:00
Calum Lind
20efcfd345
Release 2.1.0 2022-06-28 19:52:35 +01:00
Calum Lind
19dba297ef
[Docs] Fix ReadTheDocs theme rendering issue
Fix rendering issues in ReadTheDocs by specifying latest version of
sphinx-rtd-theme. Normally not an issue to install this latest
dependency from doc/requirement.txt but RTD installs in the env an older
version (<0.5) before running requirements file install
thus sphinx-rtd-theme is not upgraded unless a version is specified.

See-also: https://github.com/readthedocs/sphinx_rtd_theme/issues/1115
2022-06-28 19:45:21 +01:00
N9199
cbacaf0545
[Core] Fix typo in AUTH_LEVEL_MAPPING
Closes: https://github.com/deluge-torrent/deluge/pull/387
2022-06-19 19:02:11 +01:00
Calum Lind
75db47fc1f
Update Changelog entries 2022-06-19 08:30:54 +01:00
Calum Lind
f1ec68704d
[CD] Cleanup pip cache in Win builds
- Use the more reliable setup-python cache
- Move the pip install to GTK install step for consistency
- Don't update pip to ensure consistent version
2022-06-13 19:37:41 +01:00
Martin Hertz
ae3fbcca77
[Packaging] Pinned Pyinstaller to v4.10 and readme update
Pin Pyinstaller to latest v4.x until issue of aborting upon missing typelibs for various unbuilt gst-modules can be properly investigated and resolved. Specific error for one of the modules being:

`36738 INFO: Loading module hook 'hook-gi.repository.Gst.py' from 'C:\\hostedtoolcache\\windows\\Python\\3.9.13\\x64\\lib\\site-packages\\PyInstaller\\hooks'...
Traceback (most recent call last):
  File "<string>", line 7, in <module>
gi.repository.GLib.GError: g-irepository-error-quark: Typelib file for namespace 'Gst', version '1.0' not found (0)
36870 ERROR: gi repository 'GIRepository 2.0' not found. Please make sure corresponding package is installed.
Traceback (most recent call last):
  File "<string>", line 4, in <module>
  File "C:\hostedtoolcache\windows\Python\3.9.13\x64\lib\site-packages\gi\__init__.py", line 139, in require_version
    raise ValueError('Namespace %s not available' % namespace)
ValueError: Namespace Gst not available`

Added `--no-index` to ensure pip doesn't install from Pypi

Closes: https://github.com/deluge-torrent/deluge/pull/386
2022-06-13 19:31:54 +01:00
Calum Lind
6a10e8f3cd
[Packaging] Bump Win dependencies
* Update Twisted and libtorrent to latest releases
* Update to v3 github actions that now use node 16
2022-06-12 20:05:04 +01:00
ibizaman
b0dba97fec
[Web] Accept charset in content-type for json messages
Trac: https://dev.deluge-torrent.org/ticket/3521
Closes: https://github.com/deluge-torrent/deluge/pull/385
2022-06-12 16:08:36 +01:00
Martin Hertz
d7c520c85e
[WebUI] Fixed 'Complete Seen' and 'Completed' sorting
Closes: https://github.com/deluge-torrent/deluge/pull/384
2022-05-17 22:45:37 +01:00
Martin Hertz
ee3180fd94
[Notifications] Fix UnicodeEncodeError upon non-ascii torrent name
smtplib.SMTP.sendmail expects 'msg' in string of ascii chars or bytes,
where the former gets encoded to bytes through ascii codec, hence
raising said error, but now fixed by encoding to bytes ourself through
utf-8

Closes: https://github.com/deluge-torrent/deluge/pull/383
2022-05-17 22:42:05 +01:00
Chase Sterling
47e548fdb5
[Core] Refactor prefetch_metadata for more clarity
Just trying to clean up some of the more complicated callback logic.

Notable changes:

* The test was awaiting a DeferredList. By default that will eat
exceptions and just add them to the result list (including test
assertion exceptions.) Added fireOnOneErrback=True to make sure that
wasn't happening.  * Moved the logic for multiple calls to await the
same response into torrentmanager from core, so no matter where the
prefetch is called from it will wait for the original call.
* Implemented the multiple calls with an explicit queue of waiting
callbacks, rather than a callback callback chain.  * Moved to one inline
async function rather than split into a main and callback after alert
function.
* Added some more type hints to the stuff I changed.

Adjusted test since we are using prefetch as an async function now
we have to schedule the alert to come after we start awaiting the
prefetch call.

Closes: https://github.com/deluge-torrent/deluge/pull/368
2022-05-17 22:15:55 +01:00
Calum Lind
cd63efd935
[Console] Fix curses.init_pair raise ValueError on Py3.10
Fix ValueError crash for console users with Python 3.10

Trac: https://dev.deluge-torrent.org/ticket/3518
See-also: https://docs.python.org/3/whatsnew/3.10.html#curses
2022-05-01 21:09:41 +01:00
Calum Lind
7f0a380576
[Core] Cleanup temp files in add_torrent_url
Temporary torrent files are not deleted by add_torrent_url. Not as big a
problem as with tracker icons pages but should be removed after use.

Fixed by updating the method to use async and a try..finally cleanup
block.

Perhaps could be refactored to not require temporary files and instead
store the downloaded torrent as object for passing to add_torrent_file.

Trac: https://dev.deluge-torrent.org/ticket/3167
2022-05-01 20:38:09 +01:00
Calum Lind
68c75ccc05
[TrackerIcons] Cleanup tmp files created by downloading page
Temporary files created while download host html page are no cleaned up
if the download fails.

Fixed by adding a 'finally' step in the callback chain to delete any
created temporary files.

Added tests to ensure the temporary files are deleted, using a fixture
that creates a known filename for the test.

Trac: https://dev.deluge-torrent.org/ticket/3167
2022-05-01 20:35:28 +01:00
Calum Lind
96a0825add
[TrackerIcon] Use httpdownloader page redirect handling
User reported infinite redirecting when attempting to fetch tracker
icon.

Fixed by allowing httpdownloader and RedirectAgent to handle page
redirects including catching infinite redirects

Trac: https://dev.deluge-torrent.org/ticket/3167
See-also: https://github.com/twisted/twisted/blob/5c24e9/src/twisted/web/client.py#L168
2022-05-01 20:35:28 +01:00
Calum Lind
61a83bbd20
[Tests] Remove winreg interface name check
GitHub CI tests on Windows failing for get_windows_interface_name so
remove the fragile tests since not a requirement to be this specific
with testing whether name exists for these methods relying on standard
lib or 3rd-party libs.
2022-05-01 20:32:57 +01:00
deaddrop9
bc6611fc0d
[AutoAdd] Verify torrent decode and errors cleanly if invalid
Watch folder was disabled in AutoAdd and torrent filename is unchanged
if an invalid torrent was added.

Trac: https://dev.deluge-torrent.org/ticket/3515
Closes: https://github.com/deluge-torrent/deluge/pull/381
2022-05-01 18:34:19 +01:00
DjLegolas
970a0ae240
[lint] update black package
Previous version (22.1.0) didn't support `click` version 8.1.0.
Updating to 22.3.0 to resolve.

See: https://github.com/psf/black/issues/2964
Closes: https://github.com/deluge-torrent/deluge/pull/382
2022-05-01 18:31:12 +01:00
Calum Lind
5acb57b5af
[CI] Use setup-python action pip cache
Replace custom pip cache that didn't work correctly on Windows with
option to use pip cache in setup-python action
2022-03-02 13:08:02 +00:00
DjLegolas
7fa0af3446
[Core] Fixed KeyError in sessionproxy after torrent delete
When several torrents are being removed from session, an exception was
being raised in a callback function `on_status` with the `torrent_id` of
one (or more) of the removed torrents.
Now we will catch when the torrent does not exist anymore and print a
debug log about it.

Closes: https://dev.deluge-torrent.org/ticket/3498
Closes: https://github.com/deluge-torrent/deluge/pull/341
2022-03-02 13:00:45 +00:00
DjLegolas
a954348567
[CI] Use libtorrent pypi install
Up until now, the linux installation source of libtorrent was launchpad,
because there was no other source, and we wanted a debug version of lt.

With pypi wheel versions now available use lt in the requirements.txt
file.

Closes: https://github.com/deluge-torrent/deluge/pull/364
2022-03-02 12:55:19 +00:00
DjLegolas
13be64d355
[CI] Changed tested python version to 3.7 and 3.10
We cannot add python 3.6 because there is no precompiled version of it to used.
Therefor, will be using 3.7 as the minimum version in CI.
In addition, dropped version limits from pytest.
2022-03-02 12:53:29 +00:00
Calum Lind
11fe22e4cd
[Tests] Remove reference to Twisted Trial
With the move to pytest remove remainings documentation or comments that
refer to Trial.
2022-03-02 12:45:15 +00:00
Calum Lind
a683b7e830
[Tests] Skip SNI icon test
The SNI icon test is failing due to seo.com removing their favicon.

A new test to replace it would be needed to check SNI support. Ideally
new tests would not rely on external sites.
2022-03-02 12:18:05 +00:00
marik
b0f80f9654
[Console] Swap j and k key's behavior to fit vim mode
There is a problem in the Deluge's Console UI. This UI supports the j
and k keys as up and down, but for some reason they are inverted. This
commit inverts back the behaviour of j and k in several places.

Resolves: https://dev.deluge-torrent.org/ticket/3483
Closes: https://github.com/deluge-torrent/deluge/pull/377
2022-03-02 11:38:36 +00:00
Calum Lind
f9ca3932a8
[GTK] Remove unneeded glade icon activatable False property
This propery cause issue with added extra space in the entries, likely a
minor GTK bug and property being leftover from GTK2 migration. The
default is True and should have no effect since no icon is shown.
2022-03-02 11:29:17 +00:00
Calum Lind
5ec5271fdd
[GTK] Refactor Connection Manager Add Host dialog
Replace table with grid and use single column for entries.
2022-03-02 11:09:30 +00:00
Calum Lind
e15731fcd4
[GTK] Fix obscured port number in Connection Manager Add Host
A visual problem with the port spin button meant that the port number
was not fully visible in the entry field.

The problem is related to icon activatable property value set to False
when default is True. Although no icon is used is creates a left-hand
5px space in the entry which narrows the available text space and the
entry does not expand to account for this.

Fixed by removing primary_icon_activatable and
secondary_icon_activatable properies so that default values of True is
used.
2022-03-02 11:02:26 +00:00
Hugo Osvaldo Barrera
2962f7cd2c
[Packaging] Add systemd user services
Files should be installed into /usr/lib/systemd/user/

Unlike the existing service file, this one configures deluge to run as a
desktop session user. The difference between the services files is the
use of multi-user.target in system service which does not exist for user
services so requires default.target.

Including the Slice indicates to the service manager that this is a
background service. This can be used to handle OOM situations, or
prioritising foreground processes. There's no equivalent for system
services.

Refs: https://dev.deluge-torrent.org/ticket/2034
Closes: https://github.com/deluge-torrent/deluge/pull/380
2022-03-02 09:26:23 +00:00
tbkizle
c89a366dfb
[Hostlist] Support IPv6 in host lists
socket.gethostbyname does not support IPv6 name resolution, and
getaddrinfo() should be used instead for IPv4/v6 dual stack support.

Closes: https://github.com/deluge-torrent/deluge/pull/376
2022-02-18 23:05:12 +00:00
Calum Lind
5f8acabb81
[Console] Fix torrent details status error
The torrent status num_peers and num_seeds was replaced for session
status keys by accident as part of replacing deprecated session keys
so revert those changes

Ref: 2bd095e5bf
2022-02-17 20:30:05 +00:00
Calum Lind
055a84bb15
[Core] Fix Twisted fromCoroutine AttrError
Users with older versions of Twisted <= 21.2 were encoutering the
following error:

    File "/home/calum/projects/deluge/deluge/decorators.py", line 191, in activate
      d = defer.Deferred.fromCoroutine(self.coro)

    builtins.AttributeError: type object 'Deferred' has no attribute 'fromCoroutine'

Fixed by falling back to ensureDeferred since fromCoroutine was
introduced in Twisted 21.2 as a saner name for handling of coroutines.

Ref: https://twistedmatrix.com/trac/ticket/9825
2022-02-16 16:20:54 +00:00
Calum Lind
03938839e0
[Docs] Add permanent discord invite link
Default discord invites expire after 7 days so replace with non-expiring
invite
2022-02-15 15:30:08 +00:00
Chase Sterling
8ff4683780
Automatically refresh and expire the torrent status cache.
Stop at ratio was not working when no clients were connected, because
it was using a cached version of the torrent status, and never calling
for a refresh. When a client connected, it called for the refresh and
started working properly.

Closes: https://dev.deluge-torrent.org/ticket/3497
Closes: https://github.com/deluge-torrent/deluge/pull/369
2022-02-15 15:14:40 +00:00
Calum Lind
62a4052178
[Docs] Remove custom mock to fix autodoc typing errors
If a libtorrent return type was specified e.g.

   def get_lt_status(self) -> 'lt.torrent_status'

Even as a string autodoc_typehints module would raise and error:

    Handler <function process_docstring at 0x7f6c16c8ec10> for event 'autodoc-process-docstring' threw an exception (exception: getattr(): attribute name must be string)

This was a result of using a custom mock in Sphinx autodoc config and
this Mock object name or qualname returns an object instead of str.

Testing with putting modules in autodoc_mock_imports again showed no
issues so removing custom mock

Ref: https://github.com/tox-dev/sphinx-autodoc-typehints/issues/220
2022-02-15 11:49:54 +00:00
Calum Lind
8ece036770
[WebUI] Move HTML entity encoding to client
We should not be mangling the torrent data in the JSON API since this
can have unintended consquences with names and filepaths that can be
edited. If we escape those symbols in the JSON API then the data no
longer matches that stored by core. Therefore shift the encoding to the
client and consider dealing separetely with these entities when the user
first adds a torrent.

* Created a modified htmlEncode in Deluge Formatter based on extjs
method that also encodes single quotes.
* Removed renderers in ListViews since only templates specified via tpl
are used and any render attribute specified was a no-op.
* Removed old buggy escapeHtml

Resolves: https://dev.deluge-torrent.org/ticket/3459
Ref: https://docs.sencha.com/extjs/6.5.3/modern/src/String.js.html#Ext.String-method-htmlEncode
Ref: https://docs.sencha.com/extjs/3.4.0/source/Format.html#Ext-util-Format-method-htmlEncode
2022-02-14 18:44:19 +00:00
Calum Lind
a5503c0c60
[WebUI] Fix encoding HTML entities for torrent attributes
Ensure all torrent attributes that might contain malicious HTML entities
are encoded.

By allowing HTML entities to be rendered it enable malicious torrent
files to perform XSS attacks.

Resolves: https://dev.deluge-torrent.org/ticket/3459
2022-02-14 18:43:20 +00:00
Calum Lind
f754882498
[GTK] Increase connection mgr default height
Could not see more than one host when connection manager opens so need to
scroll or resize window

Increased default height to now show three hosts when first opens

Closes: https://dev.deluge-torrent.org/ticket/3431
2022-02-13 14:45:29 +00:00
Henry Kwan
191549074c
[GTK] Fix ui logic/bug of checked move_completed
if move_completed is checked/True, options should be updated, not the
other way round

The path was updated the first time the move_completed option is selected
and then ignored on further updated to the path.

Fixed by checking instead if the path has changed.

Closes: https://github.com/deluge-torrent/deluge/pull/374
2022-02-13 13:55:54 +00:00
Calum Lind
2ec6e10c8e
[Lint] Update linter version and fix issues
Notable changes:

* Prettier >=2.3 with more consistent js assignments
* Black now formats docstrings
* Added isort to list of autoformaters
* Update flake8 config for v4

Ref: https://prettier.io/blog/2021/05/09/2.3.0.html
2022-02-13 13:38:27 +00:00
DjLegolas
2bd095e5bf
[Core] Stopped using libtorrent deprecated functions
As part of the process of adding support to LT 2.0, we should stop using
all deprecated function, as some (if not all) were removed.
For this process, we should use the LT 1.2 upgrade (guide)[1].

The change includes:
* stop using file entries directly
* start using the torrent handle's set/unset flags
* stop using url key in add_torrent_params (for magnet)
* stop accessing resume_data from save_resume_data_alert
* stop using deprecated session status keys in UI

[1] https://libtorrent.org/upgrade_to_1.2-ref.html
Closes: https://dev.deluge-torrent.org/ticket/3499
Closes: https://github.com/deluge-torrent/deluge/pull/342
2022-02-13 11:36:04 +00:00
DjLegolas
513d5f06e5
[lt] Upgraded libtorrent minimum version to 1.2
As part of the preparations for libtorrent 2.0, we should stop supporting
lower versions of it.
2022-02-13 11:32:30 +00:00
Chase Sterling
a1da2058bc
[Core] Enable file_completed_alert handling.
Without adding file_progress to the alert mask, the
TorrentFileCompletedEvent would never fire.

Closes: https://dev.deluge-torrent.org/ticket/3421
Closes: https://github.com/deluge-torrent/deluge/pull/370
Ref: https://libtorrent.org/upgrade_to_1.2-ref.html
2022-02-13 11:25:45 +00:00
Calum Lind
af26fdfb37
[GTK] Fix adding daemon accounts
Errors were raised when trying to add a new daemon account due to dialog
being destroyed before looking up widget values.

Fixed by saving widget values before destroying.

Refactored code to be simplified with a named tuple for the account
details instead of separate attributes and modernized the preferences
dialog creation and account saving by replacing callback functions.
2022-02-12 23:40:00 +00:00
bendikro
66b5a2fc40
[Console] Fix incorrect test for when a host is online
The tests in connectionmanager for when a host is online are broken
and always considers a host as online.

When an error occurs in e.g. in _on_connect_fail, report_message()
in PopupsHandler expects the message to be string, not a Twisted Failure.
Fix by checking if message is string and log a warning before converting
to string.

Closes: https://github.com/deluge-torrent/deluge/pull/277
2022-02-12 19:03:52 +00:00
DjLegolas
29f0789223
[plugins] Add dev links script for Windows
Currently, when creating a new plugin, a script for creating
the dev links was created for *NIX systems only.
Now, it will detect the system type and create the correct
script:
Windows: create_dev_links.bat
*NIX: create_dev_links.sh

Closes: https://github.com/deluge-torrent/deluge/pull/257
2022-02-12 17:59:37 +00:00
DjLegolas
f8f997a6eb
[Config] Fix callLater func missing args
In a6840296, a refactor to the `config` class was introduced.
The change included an internal wrapper for `reactor.callLater`, for lazy
import, but didn't wrap it correctly and therefor, no args/kwargs were
passed to the wrapped method.
Furthermore, the exception was silently ignored.
This caused changes to be ignored and not applied, including
`preferencesmanager._on_config_value_change` callback.

Closes: https://github.com/deluge-torrent/deluge/pull/372
2022-02-12 17:14:19 +00:00
Chase Sterling
374997a8d7
[Tests] Make file priority test more consistent.
Our file priority test was using time.sleep to wait until libtorrent
had processed the command. This was sometimes not long enough and the
test would fail. On libtorrent 2.0.3+ there is an alert when the
process has finished, switch to waiting for that in this test to make
the test more consistent. On older libtorrent, make the delay a bit
longer, to try to make the test more consistent there as well.

Closes: https://github.com/deluge-torrent/deluge/pull/373
2022-02-12 17:12:05 +00:00
Chase Sterling
dabb505376
[Core] Document all exported core methods with type hints
Standardize docstrings in core.py to google standard.
Remove type hints in docstrings in favor of the ones in method signatures.

Use function signature type hints in autodoc generated docs.

Change Deferred type hints to strings.

Older versions of twisted (<21.7) don't
support generic Deferred type hinting,
this prevents crashes on those versions.

Closes: https://github.com/deluge-torrent/deluge/pull/359
2022-02-11 08:48:58 +00:00
Chase Sterling
aa74261d50
[GtkUI] Fix ETA being copied to neighboring empty cells
An optimization that avoided re-rendering treeview cells sometimes
went wrong, and rendered a value from the wrong row when moving
the mouse around the torrentview window.

Closes: https://dev.deluge-torrent.org/ticket/3500
Closes: https://github.com/deluge-torrent/deluge/pull/371
2022-02-11 08:31:03 +00:00
Calum Lind
b29829f571
[CI] Fix package build error
Not all dependencies were installed due to adding a comment in the
middle of the pip install command

Also need to specify Twisted extras to match requirement.txt
2022-02-09 20:39:48 +00:00
Calum Lind
d559f67ab9
[Packaging] Fix pyinstaller to find installed deluge package data
Instead of relying on the source code paths use the pip installed Deluge
package data.
2022-02-09 19:53:17 +00:00
Calum Lind
d4f8775f44
[CI] Use working dir to shorten commands
Making the workflows more readable
2022-02-09 19:53:17 +00:00
Calum Lind
50647ab3a5
[CI] Replace custom twisted package with pre-release
Fix for Windows simulate error has been merged and in 22.2.0rc

Ref: https://github.com/twisted/twisted/pull/1679
2022-02-09 19:53:17 +00:00
Calum Lind
90744dc2e6
[GTK] Workaround crash on Windows with ico or gif icons
A problem with GdkPixbuf loaders on Windows causes a hard crash when
attempting to load ico or gif tracker icons.

Added a workaround by skipping these icon types until a more permanent
solution is found.

Ref: https://dev.deluge-torrent.org/ticket/3501
2022-02-09 19:53:17 +00:00
Calum Lind
24a3987c3a
[GTK] Refactor out get_pixbuf_at_size
The functionality of get_pixbuf and get_pixbuf_at_size is almost
identical so reuse get_pixbuf with an optional size arg.
2022-02-09 19:53:15 +00:00
Calum Lind
e87236514d
[Build] Fix entry point build errors
Fixed a mistake settings entry points in setup.py. Replaced with simpler
logic since gui_scripts only affect Windows.

Fixed entry point changes affecting pyinstaller build

Corrected deluge-web.exe to have no console instead of
deluge-web-debug.exe
2022-02-06 21:02:43 +00:00
Chase Sterling
2fb41341c9
[Core] Convert inlineCallbacks to maybe_coroutine
Make logging functions synchronous.

They were not calling any async functions, and wrapping them in
maybe_coroutine caused reactor to be imported before gtkui could
install the gtk reactor.

Closes: https://github.com/deluge-torrent/deluge/pull/353
2022-02-06 16:45:37 +00:00
Chase Sterling
b76f2c0f20
[Decorators] Add maybe_coroutine decorator
- Clean up callback hell by making more code inline
- Use async/await syntax as it has more modern niceties than inlineCallbacks
  - Also gets us closer if we want to transition to asyncio in the future
  - `await` is usable in places that `yield` is not. e.g. `return await thething` `func(await thething, 'otherparam')`
  - IDEs know async semantics of async/await syntax to help more than with `inlineCallbacks`
- `maybe_coroutine` decorator has nice property (over `ensureDeferred`) that when used in a chain of other coroutines, they won't be wrapped in deferreds on each level, and so traceback will show each `await` call leading to the error.
- All async functions wrapped in `maybe_coroutine` are 100% backwards compatible with a regular Deferred returning function. Whether called from a coroutine or not.
- Use Deferred type hints as strings since older versions of twisted
(<21.7) don't support generic Deferred type hinting.
2022-02-06 16:42:13 +00:00
Calum Lind
bd88f78af6
[Plugins] Fix namespace deprecation warning
The plugin namespace was changed from deluge.plugins to deluge_
in 535b13b5f1b7b7 but deprecation warning was not updated.
2022-02-06 16:27:47 +00:00
Calum Lind
bf97bec994
[Config] Add mask_funcs to help mask passwords in logs
Added a new Config class parameter `log_mask_funcs` to enable config
instances hide sensitive information that would normally appear in
config debug logs.

Added mask password function to hostlist to replace passwords with '*'s
in logs.

Closes: https://github.com/deluge-torrent/deluge/pull/363
2022-02-06 16:15:34 +00:00
Calum Lind
a27a77f8c1
[Windows] Use gui_scripts for web and daemon entry points
Hide the console cmd popup when using deluge-web.exe ordeluged.exe on
Windows.

By using gui_scripts it will disable stdin and stdout for these
executable but there are `-debug` versions available if that is
required.

Ref: https://packaging.python.org/en/latest/specifications/entry-points/#use-for-scripts
2022-02-06 15:15:25 +00:00
kingamajick
e8fd07e5e3
[Console] Add the torrent label to info command
Closes: https://dev.deluge-torrent.org/ticket/1556
Closes: https://github.com/deluge-torrent/deluge/pull/356
2022-02-06 15:11:29 +00:00
Calum Lind
1089adb844
Revert "[Core] Document all exported core methods with type hints"
Typing is broken with older versions of Twisted e.g. with v21.2

    deluge/deluge/core/core.py", line 404, in Core
        ) -> defer.Deferred[str]:
    TypeError: 'type' object is not subscriptable

Also it might not be compatible with Python 3.6 or 3.7 with use of
certain types such as dict rather than Dict

This reverts commit 4096cdfdfe4fc7e42746cedd868bd7d8fea99f23.

Ref: https://twistedmatrix.com/trac/ticket/9816
2022-02-05 17:50:54 +00:00
Chase Sterling
4096cdfdfe
[Core] Document all exported core methods with type hints
Standardize docstrings in core.py to google standard.
Remove type hints in docstrings in favor of the ones in method signatures.

Use function signature type hints in autodoc generated docs.

Closes: https://github.com/deluge-torrent/deluge/pull/359
2022-02-05 17:23:50 +00:00
Calum Lind
099077fe20
[Config] Replace custom property decorator 2022-02-05 16:13:10 +00:00
Calum Lind
a684029602
[Config] Refactor config class
* Refactored duplication with setting config key and logging
* Simplified lazy importing reactor for callLater. This lazy importing
is required for testing and also prevents Gtk UI lockup if reactor
imported in Config.
* Fixed saving config to file when setting a key that doesn't exist yet.
This was due to returning early in the set_item method.
* Added a `default` arg to set_item to prevent saving to file when only
setting a default value for a key in init.
* Moved casting value to existing key type from set_item to dedicated
function.
2022-02-05 16:01:22 +00:00
doadin
8b0c8392b6
[Log] Fix crash logging to Windows protected folder
Have the log dir be a protected windows folder and Deluge crashes.
Windows blocks access to the dir and so it fails. It will fail trying to write
to any protected folder. Should probably just pass on the error maybe
and maybe log to stdout and log a message saying access was blocked or
something.

    .\deluge-debug -L debug -l E:\Documents\deluge.log
    ...
    Failed to execute script 'deluge-debug-script' due to unhandled exception!

Closes: https://dev.deluge-torrent.org/ticket/3502
Closes: https://github.com/deluge-torrent/deluge/pull/358
2022-02-03 22:46:33 +00:00
Chase Sterling
222aeed2f3
Remove legacy PY2 sys.argv unicode handling
Fixed crash when sys.stdout was None

When using pythonw on windows, sys.stdout and stdin are None. We had a
legacy unicode_argv handler that was crashing in this instance. Just
removed that, since sys.argv is always unicode on python 3 to fix the
crash.

Closes: https://github.com/deluge-torrent/deluge/pull/361
2022-02-03 22:38:37 +00:00
Chase Sterling
ece31cf3cf
[Tests] Transition tests to pure pytest
Convert all the twisted.trial tests to pytest_twisted. Also move off of unittest.TestCase as well. Seems there were several tests which weren't actually testing what they should, and also some code that wasn't doing what the broken test said it should.

Goals:

    Remove twisted.trial tests
    Move to pytest fixtures, rather than many classess and subclasses with setup and teardown functions
    Move away from self.assertX to assert style tests
    FIx broken tests

Going forward I think these should be the goals when adding/modifying tests:

* Don't use BaseTest or set_up tear_down methods any more. Fixtures should be used either in the test module/class, or make/improve the ones available in conftest.py
* For sure don't use unittest or twisted.trial, they mess up the pytest stuff.
* Prefer pytest_twisted.ensureDeferred with an async function over inlineCallbacks.
  - I think the async function syntax is nicer, and it helps catch silly mistakes, e.g. await None is invalid, but yield None isn't, so if some function returns an unexpected thing we try to await on, it will be caught earlier. (I struggled debugging a test for quite a while, then caught it immediately when switching to the new syntax)
  - Once the maybe_coroutine PR goes in, using the async syntax can also improve tracebacks when debugging tests.

Things that should probably be cleaned up going forward:

* Remove BaseTestCase
* Remove the subclasses like DaemonBase in favor of new fixtures.
  * I think there are some other utility subclasses that could be removed too
* Perhaps use parameterization in the ui_entry tests, rather that the weird combination of subclasses and the set_var fixture I mixed in.
* Convert some of the callback stuff to pytest_twisted.ensureDeferred tests, just for nicer readability

Details relating to pytest fixtures conftest.py in root dir:
 * https://github.com/pytest-dev/pytest/issues/5822#issuecomment-697331920
 * https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files

Closes: https://github.com/deluge-torrent/deluge/pull/354
2022-02-03 22:29:32 +00:00
Calum Lind
0fbb3882f2
[CI] Fix checkout action missing fetch depth
Need a fetch depth greater than 1 to find latest tag.
2022-02-01 06:45:10 +00:00
Calum Lind
73394f1fc5
[CI] Fix run manual packaging workflow for tag
To allow packaging any commit the workflow needs to separately checkout
the source code from the current code containing the packaging scripts.
2022-01-30 17:42:15 +00:00
Calum Lind
9b043cf2c1
[CI] Allow manual specifying tag in packaging workflow 2022-01-30 17:03:33 +00:00
DjLegolas
1cd005c272
[Gtk] Fixed edit torrents dialogs windows close issues
Up until now, when closing the Add or Edit dialogs, of the `Edit Torrents`, using the top-right X
button or using the Escape key, they were being destroyed without any way to reopening them, and
it was breaking the `Edit Torrents` window itself.
Now both dialogs have the right handlers for handing the closing process without breaking anything.

Closes: deluge-torrent/deluge#324
Closes: https://dev.deluge-torrent.org/ticket/2434
2022-01-30 16:26:44 +00:00
Facundo Acevedo
4107bf8f25
[Blocklist] Add frequency unit to interval label
Closes: https://dev.deluge-torrent.org/ticket/3492
Closes: https://github.com/deluge-torrent/deluge/pull/322
2022-01-30 16:21:39 +00:00
Chase Sterling
49bedda956
[Docs] Add discord link to readme
Closes: https://github.com/deluge-torrent/deluge/pull/350
2022-01-30 16:17:05 +00:00
tbkizle
540d557cb2
[Common] Add is_interface to validate network interfaces
Libtorrent now supports interface names instead of just IP address so
add new common functions to validate user input.

* Added is_interface that will verify if a libtorrent interface of name
or IP address.
* Added is_interface_name to verify that the name supplied is a valid
network interface name in the operating system.
  On Windows sock.if_nameindex() is only supported on 3.8+ and does not
return a uuid (required by libtorrent) so use ifaddr package. Using git
commit version for ifaddr due to adapter name decode bug in v0.1.7.
On other OSes attempt to use stdlib and fallback to ifaddr if installed
otherwiser return True.
* Added tests for is_interface & is_interface_name
* Updated UIs with change from address to interface
* Updated is_ipv6 and is_ipv4 to used inet_pton; now supported on
Windows.

Ref: https://github.com/pydron/ifaddr/pull/32
Closes: https://github.com/deluge-torrent/deluge/pull/338
2022-01-30 16:13:27 +00:00
Chase Sterling
d8acadb085
[Tests] fix/enable most ui tests on Windows
Closes: https://github.com/deluge-torrent/deluge/pull/348
2022-01-26 18:44:54 +00:00
Chase Sterling
932c3c123f
[Tests] fix torrentview tests (new default column was added) 2022-01-26 18:44:48 +00:00
Chase Sterling
986375fa86
[Tests] Make sure to return exit code still when ending test daemon
Use a separate Client instance to call test daemon shutdown
2022-01-26 18:44:48 +00:00
Chase Sterling
4497c9bbcc
[Tests] Escape backslashes in filename in webserver test 2022-01-26 18:44:47 +00:00
Chase Sterling
23f7c4dd6e
[Tests] Change example files in files tab test to sort the same on linux/windows 2022-01-26 18:44:47 +00:00
Chase Sterling
a41f950d09
[Tests] Enable more tests that now work on Windows 2022-01-26 18:44:47 +00:00
Chase Sterling
209716f7cd
[Tests] Shutdown test daemon cleanly using rpc. (needed for Windows)
Escape backslashes in config path for test daemon startup.
2022-01-26 18:44:47 +00:00
Chase Sterling
3dca30343f
[Tests] Make failure message more clear when test daemon doesn't shut down cleanly 2022-01-26 18:44:47 +00:00
Chase Sterling
71cde7c05e
[Tests] Enable unicode path test on Windows 2022-01-26 18:44:47 +00:00
Chase Sterling
dbf3495c4e
[Tests] Fix erroneous windows line endings in test state file 2022-01-26 18:44:47 +00:00
Chase Sterling
fffc6ab7d7
[Tests] Enable metafile test on Windows 2022-01-26 18:44:46 +00:00
Chase Sterling
a73e01f89f
[Tests] Fix maketorrent test on Windows 2022-01-26 18:44:46 +00:00
Chase Sterling
87ec04af16
Fix crash when logging errors initializing gettext 2022-01-26 18:44:46 +00:00
Chase Sterling
d8746a8852
[Core] Return plugin keys with get_torrents_status
When requesting all keys, get_torrents_status was missing plugin added keys
This commit brings the behavior in line with get_torrent_status, and deluge 1.3

Closes: https://dev.deluge-torrent.org/ticket/3357
Closes: https://github.com/deluge-torrent/deluge/pull/347
2022-01-26 18:40:16 +00:00
DjLegolas
7c9a542006
[GTK] Hide account password length in log
We should not let anyone know the account's password length,
as it can help to crack it.
Instead, we will print a constant amount (10) of asterisks.

Closes: https://github.com/deluge-torrent/deluge/pull/346
2022-01-23 16:39:24 +00:00
Cirno the Strongest
e75ef7e31f
[Packaging] Simplify PyInstaller spec file
This makes the process of editing the file much more pleasant and
removes duplicate code.

Fixed collecting twisted package which brings both build speed
improvements but also decreases package size, as it stops PyInstaller
from bundling tests (actually, some tests might even execute during
import and break build if they're designed to throw!) used by
PyInstaller

Closes: https://github.com/deluge-torrent/deluge/pull/342
2022-01-23 16:04:33 +00:00
DjLegolas
4f87612a0f
[Common] Replace distro.linux_distribution function
As of distro (1.6.0)[1], this function is marked as deprecated.
Instead, we will use the underlying functions directly, as explained in the (docs)[2].

[1] https://github.com/python-distro/distro/issues/263#issuecomment-927098357
[2] https://distro.readthedocs.io/en/latest/#distro.linux_distribution

Closes: https://github.com/deluge-torrent/deluge/pull/345
2022-01-22 12:06:38 +00:00
tbkizle
2cad0f46f2
[CI] Add pygame to windows package build/spec file
pygame is required by notification plugin for sounds
2022-01-21 13:12:23 +00:00
Calum Lind
5931d0cc0b
[CI] Fix package job not running with PR label
The job would only run when the PR was labeled since
`github.event.label.name` only available when `labeled` type event
recieved
2022-01-21 12:53:54 +00:00
Calum Lind
9d4ca77ef7
[CI] Cleanup packaging dependencies 2022-01-21 12:53:54 +00:00
tbkizle
ad27a278fd
[GTK UI]About Dialog Update year 2022-01-21 12:53:54 +00:00
Calum Lind
4f17fc41a5
[Packaging] Disable GTK CSD by default on Windows
CirnoT reported how they felt that GTK3 is not reliable on Windows.
Seeing some weird issues where clicking Deluge icon on taskbar does
bring window to front but doing so again does not minimize it as one
would expect. By using GTK_CSD=0 this would reduce these problems.

> If changed to 0, this disables the default use of client-side
decorations on GTK windows, thus making the window manager responsible
for drawing the decorations of windows that do not have a custom
titlebar widget.

This can be overridden by a global env var.

Ref: https://github.com/deluge-torrent/deluge/pull/331#issuecomment-1012311605
Ref: https://docs.gtk.org/gtk3/running.html
2022-01-21 12:53:54 +00:00
Calum Lind
15d2d27a53
[CI] Specify github windows server version
To ensure builds don't break avoid using windows-latest

Refs: https://github.com/actions/virtual-environments/issues/4856
2022-01-21 10:16:15 +00:00
Calum Lind
65e5010e7f
[Core] Add pygeoip dependency support
Provide support for the pure-python pygeoip as compiled GeoIP is not
always available.

Ref: https://dev.deluge-torrent.org/ticket/3271
2022-01-21 10:02:18 +00:00
DjLegolas
9b97c74025
[GTK] Added a torrent menu option for magnet copy
this will lined-up with the WebUI, which already have this option.
in addition, it will not open the Add Torrent URL dialog after copied,
which happens automatically when there is torrent/magnet URIs in the clipboard.

Closes: deluge-torrent/deluge#328
Closes: https://dev.deluge-torrent.org/ticket/3489
2022-01-21 09:41:59 +00:00
Calum Lind
d62362d6ae
[CI] Improve packaging workflow
Include arch in artifacts so they can be downloaded separately

Added libtorrent 2.0 to matrix since users often request latest
libtorrent.

Renamed workflow to make it's purpose clearer
2022-01-20 15:31:15 +00:00
Calum Lind
1a9affbbac
[Build] Add missing setuptools to requirements
Although likely to already be installed this is a runtime requirement
for Deluge
2022-01-20 14:49:53 +00:00
Calum Lind
2316088f5c
[CI] Remove PR specified branch
This branch name is the head name not the base name so prevents the job
running unless submitted has branch name that matches
2022-01-14 13:07:22 +00:00
Calum Lind
d14310078b
[CI] Fix typo in CD 2022-01-13 22:46:02 +00:00
Calum Lind
1696c69776
[CI] Fix windows build tag exclude
Fixes error:

    you may only define one of `tags` and `tags-ignore` for a single event
2022-01-13 22:36:56 +00:00
Calum Lind
5f96ea4217
[CI] Restrict creating Windows installer
Limit the running of this job by only running on develop, tags and pull
requests that have label 'windows'
2022-01-13 22:23:25 +00:00
tbkizle
491a20cb08
Fix Execute and Extractor Plugins
Include missing twisted requirements resulting in errors:

    ModuleNotFoundError: No module named 'twisted.internet.utils'
2022-01-13 22:23:25 +00:00
tbkizle
490fb898af
Build With Patched Twisted Build
Fixes TypeError in simulate call

Ref: https://twistedmatrix.com/trac/ticket/9660
Ref: https://github.com/twisted/twisted/pull/1679
2022-01-13 22:23:25 +00:00
tbkizle
560a52a443
Fix OpenSSL For Libtorrent
libtorrent + pyinstaller requires a lib(ssl/crypto)-1_1.dll and
lib(ssl/crypto)-1_1-x64.dll odd quirk but solveable by just having
two copies. Maybe later compiling our own libtorrent.
2022-01-13 22:23:25 +00:00
tbkizle
b9a208f18f
Update Windows Packaging
* Rename instances of win32 to generic win or the appropriate bit where applicable
* Remove files used in GTK2
* Add spec file for use with PyInstaller
* Remove Python bbfreeze Script
* Add Github Action To Build Releases
* Add Modified script to make files used by NSIS
* Update Readme

Closes: https://github.com/deluge-torrent/deluge/pull/331
2022-01-13 22:23:08 +00:00
Calum Lind
6da4c4bf66
Restore PY2 for 3rd-party plugins
Restored PY2 to avoid breaking compatibility with plugins that imported
PY2 from common.

Ref: https://bitbucket.org/bendikro/deluge-yarss-plugin/issues/67/deluge-210-removed-all-py2-support
2022-01-13 19:48:53 +00:00
Calum Lind
d2390cd247
[i18n] Fix load_libintl error
Fixed libintl being undefined if no library was found
2022-01-12 20:19:56 +00:00
Calum Lind
c3cd7f5e5c
[Plugins] Fix missing description with metadata 2.1
Changes to the metadata specs in v2.1 meant that Description field
might appear in the body of the message instead of as a header key.

Replaced custom parser with email parser (as outlined in the document
using compat32 policy) to simplify extracting the message header and
body.

Ref: https://dev.deluge-torrent.org/ticket/3476
Ref: https://packaging.python.org/en/latest/specifications/core-metadata/#description
2022-01-12 20:12:02 +00:00
Calum Lind
2351d65844
[Plugins] Fix and refactor get_plugin_info method
A new metadata version 2.1 has optional Description that is causing an
TypeError when looking up the key in plugin_info since clients are
assuming values are always strings but the default is None.

Fixed TypeError by ensuring that the info dict has a default empty
string set for all keys.

Separated the parsing of the pkg_info into static method to make it
easier to test.

Changed the missing plugin info to only set the Name and Version as
'not available' since all other fields are optional.

Ref: https://dev.deluge-torrent.org/ticket/3476
Ref: https://packaging.python.org/en/latest/specifications/core-metadata/#description
2022-01-12 19:19:39 +00:00
Calum Lind
e50927f575
[GTK] Fix unable to prefetch magnet in thinclient
A UnicodeDecodeError is raised in transfer module when attempting to
prefetch a magnet.

This is result of passing a Python dict containing text bytes and raw
bytes that cannot be decoded as utf-8 in rencode when recieving the
message. This could be handled in rencode by returning raw bytes if
decoding fails (perhaps with a strict mode?) however better to follow
convention of encoding raw bytes in base64 in API calls.

Fixed by retaining bencoding and encoding with base64 when sending
result.

Resolves: https://github.com/deluge-torrent/deluge/pull/334
2022-01-08 19:43:40 +00:00
Calum Lind
79b7e6093f
Fix is_url and is_infohash error with None value
Encountered a TypeError with None value passed to is_infohash function
so add guard clause.
2022-01-08 13:56:05 +00:00
DjLegolas
4f0c786649
[AutoAdd] Fixed error dialog not being shown on error
This happened due to the removal of `exception_msg` attribute, which was
removed with the changes to `RPC` protocol in commit 9b812a4.
Now we access the message using the `message` attribute.

Closes: deluge-torrent/deluge#332
Closes: https://dev.deluge-torrent.org/ticket/3069
2022-01-06 10:06:03 +00:00
DjLegolas
fca08cf583
[TrackerIcon] Fixed old-large icon removal
After downloading and resizing the new icon, we try to remove the downloaded
file, which is larger, but it fails because it tries to do so when the file
is still open, and therefor locked.
On close of the UI, we got `PermissionError` exceptions for each new icon.
2022-01-06 10:04:22 +00:00
DjLegolas
517b2c653b
[TrackerIcon] Fixed parse error on UTF-8 sites with non-english chars
When parsing the site's page in search for the FAVICON, the page gets opens.
The default file encoding in dependent on the running OS, and might not
be `UTF-8` on Windows.
Therefor, some trackers might not get their icon downloaded at all because of
an error:
`UnicodeDecodeError: 'charmap' codec can't decode byte 0x90 in position 2158: character maps to <undefined>`.
This fix adds a detection of file encoding using the optional `chardet` dependency, and also a test.

Closes: deluge-torrent/deluge#333
Closes: https://dev.deluge-torrent.org/ticket/3479
2022-01-06 10:04:19 +00:00
Patrick Byrne
44dcbee5f4
[GTK] Make combobox_window expand to width
This makes the download location entry textbox resizable which is very
useful for entering long paths.

Closes: deluge-torrent/deluge#295
2022-01-03 22:17:00 +00:00
DjLegolas
efc9f465f0
[WebUI] Define foreground and background colors
There is no promise that default bg is white and default fg is black so
define in deluge.css

Ref: https://dev.deluge-torrent.org/ticket/3435
Closes: deluge-torrent/deluge#330
2022-01-03 22:07:11 +00:00
DjLegolas
5321d24f2a
[GTK] Use GtkSpinner when testing open port
this switched was motivated by an error which happened each time the check
port button was clicked, and was caused by the GtkImage when loading the
loading.gif file on Windows:

    cannot register existing type 'GdkPixbufGdipAnim'

Closes: deluge-torrent/deluge#329
2022-01-03 22:02:53 +00:00
RedBearAK
f30f7f4629
[UI] Add Keywords property to desktop file
Deluge fails to appear in some app launchers (GNOME app search, Albert launcher) when searching for just "torrent" or other keywords, rather than "bittorrent". This is due to the lack of a Keywords header/property in its desktop entry file. Adding this line should solve the issue.

I don't know if the underscore "_" is actually necessary for this line, I just copied the appearance of the lines above it when inserting. Please check that this comes out without the underscore in the final file after processing.

Closes: deluge-torrent/deluge#323
2021-12-29 21:54:00 +00:00
DjLegolas
ec0bcc11f5
Upgrade codebase with pyupgrade (>=py3.6)
Added pyupgrade utility with manual stage to pre-commit and run on all
files.

Ref: https://github.com/asottile/pyupgrade
Closes: deluge-torrent/deluge#326
2021-12-29 21:51:07 +00:00
Calum Lind
16895b4a49
[Docs] Fix spinx-contrib-spelling build error
CI docs build was failing with the following error when using latest
sphinx-contrib-spelling 7.3.1

    error: option -j not recognized

Fixed by pinning to previous version.

GitHub-ref: https://github.com/sphinx-contrib/spelling/issues/142
2021-12-29 21:43:03 +00:00
DjLegolas
f3784723ae
[UI] Add SVG support for tracker icons
SVG files are supported by all browsers so need to support it as well,
according to https://www.w3schools.com/html/html_favicon.asp

Also, it appears as SEO.com site, which was dropped because of a cert issue,
has only SVG icon. So enabled it again.

Lastly, from python 3.2, `os.path.samefile` is supported on Windows.
So Windows will now test TrackerIcons as well.
2021-12-29 21:38:55 +00:00
DjLegolas
7f5857296e [CI] Upgrade Windows python version to 3.8 (same as linux) 2021-12-29 20:06:10 +02:00
DjLegolas
897955f0a1
Remove all Python 2 support
* Removed all __future__ imports from code
* Removed all six dependencies
* Removed all future_builtins imports
* Removed all Python 2 related code

Closes: deluge-torrent/deluge#325
2021-12-28 19:26:38 +00:00
Calum Lind
ff309ea4c5
[GtkUI] Fix ETA sorting to match WebUI
The sort for Ascending was putting longest eta first but seems more
intuitive that the smallest time to wait should be first. The WebUI
in previous commit swapped this behaviour so updating GtkUI.
2021-12-22 23:17:14 +00:00
DjLegolas
3b11613cc7
[WebUI] Fixed ETA sorting in WebUI
When sorting the according to ETA values, all torrents with infinite value were being
considered a lower value (INF -> 12 -> 32) instead of largest (12 -> 32 -> INF).
This is due to the fact that the INF symbol is placed to lower value (<= 0).
Now the lower values are being treated as the largest JS number when sorting.

Closes: https://dev.deluge-torrent.org/ticket/3413
Closes: https://github.com/deluge-torrent/deluge/pull/321
2021-12-22 22:04:24 +00:00
DjLegolas
a2d0cb7141
[Console] Removed Core dependency from Console UI
UIs should not depend on core directly, so removing the dependency in ConsoleUI.
This is done by adding a hard-coded variable.

Closes https://dev.deluge-torrent.org/ticket/3491
Closes: https://github.com/deluge-torrent/deluge/pull/320
2021-12-22 21:53:17 +00:00
DjLegolas
88ffd1b843
[Servers] Moved check_ssl_keys and generate_ssl_keys to crypto_utils.py
With this change, we drop a core dependency from the UI. This will help group together
all related functionality in one place, i.e. all security related functions.

Also updated testssl.sh version to 3.0.6 (SECURITY_TEST)

Closes: deluge-torrent/deluge#288
2021-12-20 22:09:08 +00:00
Calum Lind
6a10e57f7e
back to development 2021-12-15 19:43:39 +00:00
371 changed files with 4353 additions and 5828 deletions

1
.gitattributes vendored
View File

@ -3,3 +3,4 @@
.gitignore export-ignore .gitignore export-ignore
*.py diff=python *.py diff=python
ext-all.js diff=minjs ext-all.js diff=minjs
*.state -merge -text

100
.github/workflows/cd.yml vendored Normal file
View File

@ -0,0 +1,100 @@
name: Package
on:
push:
tags:
- "deluge-*"
- "!deluge*-dev*"
branches:
- develop
pull_request:
types: [labeled, opened, synchronize, reopened]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
ref:
description: "Enter a tag or commit to package"
default: ""
jobs:
windows_package:
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.6, 1.2.15]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- 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@v3
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
path: deluge_src
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python}}
architecture: ${{ matrix.arch }}
cache: pip
- name: Prepare pip
run: python -m pip install wheel
- name: Install GTK
run: |
$WebClient = New-Object System.Net.WebClient
$WebClient.DownloadFile("https://github.com/deluge-torrent/gvsbuild-release/releases/download/latest/gvsbuild-py${{ matrix.python }}-vs16-${{ matrix.arch }}.zip","C:\GTK.zip")
7z x C:\GTK.zip -oc:\GTK
echo "C:\GTK\release\lib" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "C:\GTK\release\bin" | Out-File -FilePath $env:GITHUB_PATH -Append
echo "C:\GTK\release" | Out-File -FilePath $env:GITHUB_PATH -Append
python -m pip install --no-index --find-links="C:\GTK\release\python" pycairo PyGObject
- name: Install Python dependencies
run: >
python -m pip install
twisted[tls]==22.4.0
libtorrent==${{ matrix.libtorrent }}
pyinstaller==4.10
pygame
-r requirements.txt
- name: Install Deluge
working-directory: deluge_src
run: |
python -m pip install .
python setup.py install_scripts
- name: Freeze Deluge
working-directory: packaging/win
run: |
pyinstaller --clean delugewin.spec --distpath freeze
- name: Fix OpenSSL for libtorrent x64
if: ${{ matrix.arch == 'x64' }}
working-directory: packaging/win/freeze/Deluge
run: |
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
run: |
python setup_nsis.py
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
- uses: actions/upload-artifact@v2
with:
name: deluge-py${{ matrix.python }}-lt${{ matrix.libtorrent }}-${{ matrix.arch }}
path: packaging/win/*.exe

View File

@ -10,6 +10,9 @@ on:
jobs: jobs:
test-linux: test-linux:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7", "3.10"]
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
@ -20,26 +23,13 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.8" python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: "requirements*.txt"
- name: Cache pip - name: Sets env var for security
uses: actions/cache@v2 if: (github.event_name == 'pull_request' && contains(github.event.pull_request.body, 'security_test')) || (github.event_name == 'push' && contains(github.event.head_commit.message, 'security_test'))
with: run: echo "SECURITY_TESTS=True" >> $GITHUB_ENV
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Add libtorrent deb repository
uses: myci-actions/add-deb-repo@8
with:
repo: deb http://ppa.launchpad.net/libtorrent.org/1.2-daily/ubuntu focal main
repo-name: libtorrent
keys: 58E5430D9667FAEFFCA0B93F32309D6B9E009EDB
key-server: keyserver.ubuntu.com
install: python3-libtorrent-dbg
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -47,6 +37,15 @@ jobs:
pip install -r requirements.txt -r requirements-tests.txt pip install -r requirements.txt -r requirements-tests.txt
pip install -e . pip install -e .
- name: Install security dependencies
if: contains(env.SECURITY_TESTS, 'True')
run: |
wget -O- $TESTSSL_URL$TESTSSL_VER | tar xz
mv -t deluge/tests/data testssl.sh-$TESTSSL_VER/testssl.sh testssl.sh-$TESTSSL_VER/etc/;
env:
TESTSSL_VER: 3.0.6
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
- name: Setup core dump directory - name: Setup core dump directory
run: | run: |
sudo mkdir /cores/ && sudo chmod 777 /cores/ sudo mkdir /cores/ && sudo chmod 777 /cores/
@ -55,9 +54,8 @@ jobs:
- name: Test with pytest - name: Test with pytest
run: | run: |
ulimit -c unlimited # Enable core dumps to be captured ulimit -c unlimited # Enable core dumps to be captured
cp /usr/lib/python3/dist-packages/libtorrent*.so $GITHUB_WORKSPACE/deluge
python -c 'from deluge._libtorrent import lt; print(lt.__version__)'; python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui or security)" deluge catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
- uses: actions/upload-artifact@v2 - uses: actions/upload-artifact@v2
# capture all crashes as build artifacts # capture all crashes as build artifacts
@ -67,7 +65,10 @@ jobs:
path: /cores path: /cores
test-windows: test-windows:
runs-on: windows-latest runs-on: windows-2019
strategy:
matrix:
python-version: ["3.7", "3.10"]
steps: steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
@ -78,26 +79,17 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.7" python-version: ${{ matrix.python-version }}
cache: "pip"
- name: Cache pip cache-dependency-path: "requirements*.txt"
uses: actions/cache@v2
with:
path: '%LOCALAPPDATA%\pip\Cache'
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('tox.ini', 'setup.py', 'requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip wheel pip install --upgrade pip wheel
python -m pip install libtorrent==1.2.*
pip install -r requirements.txt -r requirements-tests.txt pip install -r requirements.txt -r requirements-tests.txt
pip install -e . pip install -e .
- name: Test with pytest - name: Test with pytest
run: | run: |
python -c 'import libtorrent as lt; print(lt.__version__)'; python -c 'import libtorrent as lt; print(lt.__version__)';
pytest -m "not (todo or gtkui or security)" deluge pytest -v -m "not (todo or gtkui or security)" deluge

1
.gitignore vendored
View File

@ -10,7 +10,6 @@ docs/source/modules/deluge*.rst
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*.tar.* *.tar.*
_trial_temp
.tox/ .tox/
deluge/i18n/*/ deluge/i18n/*/
deluge.pot deluge.pot

View File

@ -3,32 +3,35 @@ default_language_version:
exclude: > exclude: >
(?x)^( (?x)^(
deluge/ui/web/docs/template/.*| deluge/ui/web/docs/template/.*|
deluge/tests/data/.*svg|
)$ )$
repos: repos:
- repo: https://github.com/ambv/black - repo: https://github.com/psf/black
rev: 20.8b1 rev: 22.3.0
hooks: hooks:
- id: black - id: black
name: Fmt Black name: Fmt Black
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.2.1 rev: v2.5.1
hooks: hooks:
- id: prettier - id: prettier
name: Fmt Prettier name: Fmt Prettier
# Workaround to list modified files only. # Workaround to list modified files only.
args: [--list-different] args: [--list-different]
- repo: https://gitlab.com/pycqa/flake8 - repo: https://github.com/pycqa/isort
# v3.7.9 due to E402 issue: https://gitlab.com/pycqa/flake8/-/issues/638 rev: 5.10.1
rev: 3.7.9 hooks:
- id: isort
name: Fmt isort
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks: hooks:
- id: flake8 - id: flake8
name: Chk Flake8 name: Chk Flake8
additional_dependencies: additional_dependencies:
- flake8-isort==4.0.0 - pep8-naming==0.12.1
- pep8-naming==0.11.1
args: [--isort-show-traceback]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0 rev: v4.1.0
hooks: hooks:
- id: double-quote-string-fixer - id: double-quote-string-fixer
name: Fix Double-quotes name: Fix Double-quotes
@ -40,3 +43,9 @@ repos:
args: [--fix=auto] args: [--fix=auto]
- id: trailing-whitespace - id: trailing-whitespace
name: Fix Trailing whitespace name: Fix Trailing whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v2.31.0
hooks:
- id: pyupgrade
args: [--py36-plus]
stages: [manual]

View File

@ -289,7 +289,7 @@ callbacks=cb_,_cb
# List of qualified module names which can have objects that can redefine # List of qualified module names which can have objects that can redefine
# builtins. # builtins.
redefining-builtins-modules=six.moves,future.builtins,future_builtins redefining-builtins-modules=
[TYPECHECK] [TYPECHECK]
@ -359,11 +359,6 @@ known-standard-library=
# Force import order to recognize a module as part of a third party library. # Force import order to recognize a module as part of a third party library.
known-third-party=enchant known-third-party=enchant
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
[DESIGN] [DESIGN]

View File

@ -1,5 +1,87 @@
# Changelog # Changelog
## 2.1.1 (2022-07-10)
### Core
- Fix missing trackers added via magnet
- Fix handling magnets with tracker tiers
## 2.1.0 (2022-06-28)
### Breaking changes
- Python 2 support removed (Python >= 3.6)
- libtorrent minimum requirement increased (>= 1.2).
### Core
- Add support for SVG tracker icons.
- Fix tracker icon error handling.
- Fix cleaning-up tracker icon temp files.
- Fix Plugin manager to handle new metadata 2.1.
- Hide passwords in config logs.
- Fix cleaning-up temp files in add_torrent_url.
- Fix KeyError in sessionproxy after torrent delete.
- Remove libtorrent deprecated functions.
- Fix file_completed_alert handling.
- Add plugin keys to get_torrents_status.
- Add support for pygeoip dependency.
- Fix crash logging to Windows protected folder.
- Add is_interface and is_interface_name to validate network interfaces.
- Fix is_url and is_infohash error with None value.
- Fix load_libintl error.
- Add support for IPv6 in host lists.
- Add systemd user services.
- Fix refresh and expire the torrent status cache.
- Fix crash when logging errors initializing gettext.
### Web UI
- Fix ETA column sorting in correct order (#3413).
- Fix defining foreground and background colors.
- Accept charset in content-type for json messages.
- Fix 'Complete Seen' and 'Completed' sorting.
- Fix encoding HTML entities for torrent attributes to prevent XSS.
### Gtk UI
- Fix download location textbox width.
- Fix obscured port number in Connection Manager.
- Increase connection manager default height.
- Fix bug with setting move completed in Options tab.
- Fix adding daemon accounts.
- Add workaround for crash on Windows with ico or gif icons.
- Hide account password length in log.
- Added a torrent menu option for magnet copy.
- Fix unable to prefetch magnet in thinclient mode.
- Use GtkSpinner when testing open port.
- Update About Dialog year.
- Fix Edit Torrents dialogs close issues.
- Fix ETA being copied to neighboring empty cells.
- Disable GTK CSD by default on Windows.
### Console UI
- Fix curses.init_pair raise ValueError on Py3.10.
- Swap j and k key's behavior to fit vim mode.
- Fix torrent details status error.
- Fix incorrect test for when a host is online.
- Add the torrent label to info command.
### AutoAdd
- Fix handling torrent decode errors.
- Fix error dialog not being shown on error.
### Blocklist
- Add frequency unit to interval label.
### Notifications
- Fix UnicodeEncodeError upon non-ascii torrent name.
## 2.0.5 (2021-12-15) ## 2.0.5 (2021-12-15)
### WebUI ### WebUI

View File

@ -7,7 +7,7 @@ All modules will require the [common](#common) section dependencies.
## Prerequisite ## Prerequisite
- [Python] _>= 3.5_ - [Python] _>= 3.6_
## Build ## Build
@ -23,12 +23,12 @@ All modules will require the [common](#common) section dependencies.
- [rencode] _>= 1.0.2_ - Encoding library. - [rencode] _>= 1.0.2_ - Encoding library.
- [PyXDG] - Access freedesktop.org standards for \*nix. - [PyXDG] - Access freedesktop.org standards for \*nix.
- [xdg-utils] - Provides xdg-open for \*nix. - [xdg-utils] - Provides xdg-open for \*nix.
- [six]
- [zope.interface] - [zope.interface]
- [chardet] - Optional: Encoding detection. - [chardet] - Optional: Encoding detection.
- [setproctitle] - Optional: Renaming processes. - [setproctitle] - Optional: Renaming processes.
- [Pillow] - Optional: Support for resizing tracker icons. - [Pillow] - Optional: Support for resizing tracker icons.
- [dbus-python] - Optional: Show item location in filemanager. - [dbus-python] - Optional: Show item location in filemanager.
- [ifaddr] - Optional: Verify network interfaces.
### Linux and BSD ### Linux and BSD
@ -41,8 +41,8 @@ All modules will require the [common](#common) section dependencies.
## Core (deluged daemon) ## Core (deluged daemon)
- [libtorrent] _>= 1.1.1_ - [libtorrent] _>= 1.2.0_
- [GeoIP] - Optional: IP address location lookup. (_Debian: `python-geoip`_) - [GeoIP] or [pygeoip] - Optional: IP address country lookup. (_Debian: `python-geoip`_)
## GTK UI ## GTK UI
@ -81,14 +81,12 @@ All modules will require the [common](#common) section dependencies.
[distro]: https://github.com/nir0s/distro [distro]: https://github.com/nir0s/distro
[pywin32]: https://github.com/mhammond/pywin32 [pywin32]: https://github.com/mhammond/pywin32
[certifi]: https://pypi.org/project/certifi/ [certifi]: https://pypi.org/project/certifi/
[py2-ipaddress]: https://pypi.org/project/py2-ipaddress/
[dbus-python]: https://pypi.org/project/dbus-python/ [dbus-python]: https://pypi.org/project/dbus-python/
[setproctitle]: https://pypi.org/project/setproctitle/ [setproctitle]: https://pypi.org/project/setproctitle/
[gtkosxapplication]: https://github.com/jralls/gtk-mac-integration [gtkosxapplication]: https://github.com/jralls/gtk-mac-integration
[chardet]: https://chardet.github.io/ [chardet]: https://chardet.github.io/
[rencode]: https://github.com/aresch/rencode [rencode]: https://github.com/aresch/rencode
[pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/ [pyxdg]: https://www.freedesktop.org/wiki/Software/pyxdg/
[six]: https://pythonhosted.org/six/
[xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/ [xdg-utils]: https://www.freedesktop.org/wiki/Software/xdg-utils/
[gtk+]: https://www.gtk.org/ [gtk+]: https://www.gtk.org/
[pycairo]: https://cairographics.org/pycairo/ [pycairo]: https://cairographics.org/pycairo/
@ -99,3 +97,4 @@ All modules will require the [common](#common) section dependencies.
[libnotify]: https://developer.gnome.org/libnotify/ [libnotify]: https://developer.gnome.org/libnotify/
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator [python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg [librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
[ifaddr]: https://pypi.org/project/ifaddr/

View File

@ -59,6 +59,7 @@ See the [Thinclient guide] to connect to the daemon from another computer.
- [User guide][user guide] - [User guide][user guide]
- [Forum](https://forum.deluge-torrent.org) - [Forum](https://forum.deluge-torrent.org)
- [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge) - [IRC Libera.Chat #deluge](irc://irc.libera.chat/deluge)
- [Discord](https://discord.gg/nwaHSE6tqn)
[user guide]: https://dev.deluge-torrent.org/wiki/UserGuide [user guide]: https://dev.deluge-torrent.org/wiki/UserGuide
[thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient [thinclient guide]: https://dev.deluge-torrent.org/wiki/UserGuide/ThinClient

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -15,8 +14,6 @@ Example:
>>> from deluge._libtorrent import lt >>> from deluge._libtorrent import lt
""" """
from __future__ import unicode_literals
from deluge.common import VersionSplit, get_version from deluge.common import VersionSplit, get_version
from deluge.error import LibtorrentImportError from deluge.error import LibtorrentImportError
@ -29,10 +26,10 @@ except ImportError:
raise LibtorrentImportError('No libtorrent library found: %s' % (ex)) raise LibtorrentImportError('No libtorrent library found: %s' % (ex))
REQUIRED_VERSION = '1.1.2.0' REQUIRED_VERSION = '1.2.0.0'
LT_VERSION = lt.__version__ LT_VERSION = lt.__version__
if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION): if VersionSplit(LT_VERSION) < VersionSplit(REQUIRED_VERSION):
raise LibtorrentImportError( raise LibtorrentImportError(
'Deluge %s requires libtorrent >= %s' % (get_version(), REQUIRED_VERSION) f'Deluge {get_version()} requires libtorrent >= {REQUIRED_VERSION}'
) )

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import argparse import argparse
import logging import logging
import os import os
@ -95,7 +92,7 @@ def _get_version_detail():
except ImportError: except ImportError:
pass pass
version_str += 'Python: %s\n' % platform.python_version() version_str += 'Python: %s\n' % platform.python_version()
version_str += 'OS: %s %s\n' % (platform.system(), common.get_os_version()) version_str += f'OS: {platform.system()} {common.get_os_version()}\n'
return version_str return version_str
@ -109,8 +106,8 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
line instead. This way list formatting is not mangled by textwrap.wrap. line instead. This way list formatting is not mangled by textwrap.wrap.
""" """
wrapped_lines = [] wrapped_lines = []
for l in text.splitlines(): for line in text.splitlines():
wrapped_lines.extend(textwrap.wrap(l, width, subsequent_indent=' ')) wrapped_lines.extend(textwrap.wrap(line, width, subsequent_indent=' '))
return wrapped_lines return wrapped_lines
def _format_action_invocation(self, action): def _format_action_invocation(self, action):
@ -137,7 +134,7 @@ class DelugeTextHelpFormatter(argparse.RawDescriptionHelpFormatter):
default = action.dest.upper() default = action.dest.upper()
args_string = self._format_args(action, default) args_string = self._format_args(action, default)
opt = ', '.join(action.option_strings) opt = ', '.join(action.option_strings)
parts.append('%s %s' % (opt, args_string)) parts.append(f'{opt} {args_string}')
return ', '.join(parts) return ', '.join(parts)
@ -165,7 +162,7 @@ class ArgParserBase(argparse.ArgumentParser):
self.log_stream = kwargs['log_stream'] self.log_stream = kwargs['log_stream']
del kwargs['log_stream'] del kwargs['log_stream']
super(ArgParserBase, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.common_setup = False self.common_setup = False
self.process_arg_group = False self.process_arg_group = False
@ -202,7 +199,7 @@ class ArgParserBase(argparse.ArgumentParser):
self.group.add_argument( self.group.add_argument(
'-L', '-L',
'--loglevel', '--loglevel',
choices=[l for k in deluge.log.levels for l in (k, k.upper())], choices=[level for k in deluge.log.levels for level in (k, k.upper())],
help=_('Set the log level (none, error, warning, info, debug)'), help=_('Set the log level (none, error, warning, info, debug)'),
metavar='<level>', metavar='<level>',
) )
@ -246,7 +243,7 @@ class ArgParserBase(argparse.ArgumentParser):
argparse.Namespace: The parsed arguments. argparse.Namespace: The parsed arguments.
""" """
options = super(ArgParserBase, self).parse_args(args=args) options = super().parse_args(args=args)
return self._handle_ui_options(options) return self._handle_ui_options(options)
def parse_known_ui_args(self, args, withhold=None): def parse_known_ui_args(self, args, withhold=None):
@ -262,7 +259,7 @@ class ArgParserBase(argparse.ArgumentParser):
""" """
if withhold: if withhold:
args = [a for a in args if a not in withhold] args = [a for a in args if a not in withhold]
options, remaining = super(ArgParserBase, self).parse_known_args(args=args) options, remaining = super().parse_known_args(args=args)
options.remaining = remaining options.remaining = remaining
# Handle common and process group options # Handle common and process group options
return self._handle_ui_options(options) return self._handle_ui_options(options)

View File

@ -9,13 +9,7 @@
# License. # License.
# Written by Petru Paler # Written by Petru Paler
# Updated by Calum Lind to support both Python 2 and Python 3. # Updated by Calum Lind to support Python 3.
from __future__ import unicode_literals
from sys import version_info
PY2 = version_info.major == 2
class BTFailure(Exception): class BTFailure(Exception):
@ -90,7 +84,7 @@ def bdecode(x):
return r return r
class Bencached(object): class Bencached:
__slots__ = ['bencoded'] __slots__ = ['bencoded']
@ -146,10 +140,6 @@ encode_func[dict] = encode_dict
encode_func[bool] = encode_bool encode_func[bool] = encode_bool
encode_func[str] = encode_string encode_func[str] = encode_string
encode_func[bytes] = encode_bytes encode_func[bytes] = encode_bytes
if PY2:
encode_func[long] = encode_int # noqa: F821
encode_func[str] = encode_bytes
encode_func[unicode] = encode_string # noqa: F821
def bencode(x): def bencode(x):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
# #
@ -8,25 +7,25 @@
# #
"""Common functions for various parts of Deluge to use.""" """Common functions for various parts of Deluge to use."""
from __future__ import division, print_function, unicode_literals
import base64 import base64
import binascii import binascii
import functools import functools
import glob import glob
import locale
import logging import logging
import numbers import numbers
import os import os
import platform import platform
import re import re
import socket
import subprocess import subprocess
import sys import sys
import tarfile import tarfile
import time import time
from contextlib import closing from contextlib import closing
from datetime import datetime from datetime import datetime
from io import BytesIO, open from io import BytesIO
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
import pkg_resources import pkg_resources
@ -38,14 +37,6 @@ try:
except ImportError: except ImportError:
chardet = None chardet = None
try:
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
except ImportError:
# PY2 fallback
from urllib import pathname2url, unquote_plus # pylint: disable=ungrouped-imports
from urlparse import urljoin # pylint: disable=ungrouped-imports
# Windows workaround for HTTPS requests requiring certificate authority bundle. # Windows workaround for HTTPS requests requiring certificate authority bundle.
# see: https://twistedmatrix.com/trac/ticket/9209 # see: https://twistedmatrix.com/trac/ticket/9209
if platform.system() in ('Windows', 'Microsoft'): if platform.system() in ('Windows', 'Microsoft'):
@ -53,6 +44,11 @@ if platform.system() in ('Windows', 'Microsoft'):
os.environ['SSL_CERT_FILE'] = where() os.environ['SSL_CERT_FILE'] = where()
try:
import ifaddr
except ImportError:
ifaddr = None
if platform.system() not in ('Windows', 'Microsoft', 'Darwin'): if platform.system() not in ('Windows', 'Microsoft', 'Darwin'):
# gi makes dbus available on Window but don't import it as unused. # gi makes dbus available on Window but don't import it as unused.
@ -84,7 +80,8 @@ JSON_FORMAT = {'indent': 4, 'sort_keys': True, 'ensure_ascii': False}
DBUS_FM_ID = 'org.freedesktop.FileManager1' DBUS_FM_ID = 'org.freedesktop.FileManager1'
DBUS_FM_PATH = '/org/freedesktop/FileManager1' DBUS_FM_PATH = '/org/freedesktop/FileManager1'
PY2 = sys.version_info.major == 2 # Retained for plugin backward compatibility
PY2 = False
def get_version(): def get_version():
@ -111,10 +108,8 @@ def get_default_config_dir(filename=None):
def save_config_path(resource): def save_config_path(resource):
app_data_path = os.environ.get('APPDATA') app_data_path = os.environ.get('APPDATA')
if not app_data_path: if not app_data_path:
try: import winreg
import winreg
except ImportError:
import _winreg as winreg # For Python 2.
hkey = winreg.OpenKey( hkey = winreg.OpenKey(
winreg.HKEY_CURRENT_USER, winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders', 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders',
@ -147,14 +142,14 @@ def get_default_download_dir():
try: try:
user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs') user_dirs_path = os.path.join(xdg_config_home, 'user-dirs.dirs')
with open(user_dirs_path, 'r', encoding='utf8') as _file: with open(user_dirs_path, encoding='utf8') as _file:
for line in _file: for line in _file:
if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'): if not line.startswith('#') and line.startswith('XDG_DOWNLOAD_DIR'):
download_dir = os.path.expandvars( download_dir = os.path.expandvars(
line.partition('=')[2].rstrip().strip('"') line.partition('=')[2].rstrip().strip('"')
) )
break break
except IOError: except OSError:
pass pass
if not download_dir: if not download_dir:
@ -178,8 +173,8 @@ def archive_files(arc_name, filepaths, message=None, rotate=10):
from deluge.configmanager import get_config_dir from deluge.configmanager import get_config_dir
# Set archive compression to lzma with bz2 fallback. # Set archive compression to lzma
arc_comp = 'xz' if not PY2 else 'bz2' arc_comp = 'xz'
archive_dir = os.path.join(get_config_dir(), 'archive') archive_dir = os.path.join(get_config_dir(), 'archive')
timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-') timestamp = datetime.now().replace(microsecond=0).isoformat().replace(':', '-')
@ -275,7 +270,7 @@ def get_os_version():
os_version = list(platform.mac_ver()) os_version = list(platform.mac_ver())
os_version[1] = '' # versioninfo always empty. os_version[1] = '' # versioninfo always empty.
elif distro: elif distro:
os_version = distro.linux_distribution() os_version = (distro.name(), distro.version(), distro.codename())
else: else:
os_version = (platform.release(),) os_version = (platform.release(),)
@ -441,22 +436,22 @@ def fsize(fsize_b, precision=1, shortform=False):
""" """
if fsize_b >= 1024 ** 4: if fsize_b >= 1024**4:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
fsize_b / 1024 ** 4, fsize_b / 1024**4,
tib_txt_short if shortform else tib_txt, tib_txt_short if shortform else tib_txt,
) )
elif fsize_b >= 1024 ** 3: elif fsize_b >= 1024**3:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
fsize_b / 1024 ** 3, fsize_b / 1024**3,
gib_txt_short if shortform else gib_txt, gib_txt_short if shortform else gib_txt,
) )
elif fsize_b >= 1024 ** 2: elif fsize_b >= 1024**2:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
fsize_b / 1024 ** 2, fsize_b / 1024**2,
mib_txt_short if shortform else mib_txt, mib_txt_short if shortform else mib_txt,
) )
elif fsize_b >= 1024: elif fsize_b >= 1024:
@ -508,28 +503,28 @@ def fspeed(bps, precision=1, shortform=False):
""" """
if bps < 1024 ** 2: if bps < 1024**2:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
bps / 1024, bps / 1024,
_('K/s') if shortform else _('KiB/s'), _('K/s') if shortform else _('KiB/s'),
) )
elif bps < 1024 ** 3: elif bps < 1024**3:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
bps / 1024 ** 2, bps / 1024**2,
_('M/s') if shortform else _('MiB/s'), _('M/s') if shortform else _('MiB/s'),
) )
elif bps < 1024 ** 4: elif bps < 1024**4:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
bps / 1024 ** 3, bps / 1024**3,
_('G/s') if shortform else _('GiB/s'), _('G/s') if shortform else _('GiB/s'),
) )
else: else:
return '%.*f %s' % ( return '%.*f %s' % (
precision, precision,
bps / 1024 ** 4, bps / 1024**4,
_('T/s') if shortform else _('TiB/s'), _('T/s') if shortform else _('TiB/s'),
) )
@ -552,9 +547,9 @@ def fpeer(num_peers, total_peers):
""" """
if total_peers > -1: if total_peers > -1:
return '{:d} ({:d})'.format(num_peers, total_peers) return f'{num_peers:d} ({total_peers:d})'
else: else:
return '{:d}'.format(num_peers) return f'{num_peers:d}'
def ftime(secs): def ftime(secs):
@ -580,17 +575,17 @@ def ftime(secs):
if secs <= 0: if secs <= 0:
time_str = '' time_str = ''
elif secs < 60: elif secs < 60:
time_str = '{}s'.format(secs) time_str = f'{secs}s'
elif secs < 3600: elif secs < 3600:
time_str = '{}m {}s'.format(secs // 60, secs % 60) time_str = f'{secs // 60}m {secs % 60}s'
elif secs < 86400: elif secs < 86400:
time_str = '{}h {}m'.format(secs // 3600, secs // 60 % 60) time_str = f'{secs // 3600}h {secs // 60 % 60}m'
elif secs < 604800: elif secs < 604800:
time_str = '{}d {}h'.format(secs // 86400, secs // 3600 % 24) time_str = f'{secs // 86400}d {secs // 3600 % 24}h'
elif secs < 31449600: elif secs < 31449600:
time_str = '{}w {}d'.format(secs // 604800, secs // 86400 % 7) time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
else: else:
time_str = '{}y {}w'.format(secs // 31449600, secs // 604800 % 52) time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
return time_str return time_str
@ -644,17 +639,17 @@ def tokenize(text):
size_units = [ size_units = [
{'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'}, {'prefix': 'b', 'divider': 1, 'singular': 'byte', 'plural': 'bytes'},
{'prefix': 'KiB', 'divider': 1024 ** 1}, {'prefix': 'KiB', 'divider': 1024**1},
{'prefix': 'MiB', 'divider': 1024 ** 2}, {'prefix': 'MiB', 'divider': 1024**2},
{'prefix': 'GiB', 'divider': 1024 ** 3}, {'prefix': 'GiB', 'divider': 1024**3},
{'prefix': 'TiB', 'divider': 1024 ** 4}, {'prefix': 'TiB', 'divider': 1024**4},
{'prefix': 'PiB', 'divider': 1024 ** 5}, {'prefix': 'PiB', 'divider': 1024**5},
{'prefix': 'KB', 'divider': 1000 ** 1}, {'prefix': 'KB', 'divider': 1000**1},
{'prefix': 'MB', 'divider': 1000 ** 2}, {'prefix': 'MB', 'divider': 1000**2},
{'prefix': 'GB', 'divider': 1000 ** 3}, {'prefix': 'GB', 'divider': 1000**3},
{'prefix': 'TB', 'divider': 1000 ** 4}, {'prefix': 'TB', 'divider': 1000**4},
{'prefix': 'PB', 'divider': 1000 ** 5}, {'prefix': 'PB', 'divider': 1000**5},
{'prefix': 'm', 'divider': 1000 ** 2}, {'prefix': 'm', 'divider': 1000**2},
] ]
@ -712,6 +707,9 @@ def is_url(url):
True True
""" """
if not url:
return False
return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp') return url.partition('://')[0] in ('http', 'https', 'ftp', 'udp')
@ -726,6 +724,9 @@ def is_infohash(infohash):
bool: True if valid infohash, False otherwise. bool: True if valid infohash, False otherwise.
""" """
if not infohash:
return False
return len(infohash) == 40 and infohash.isalnum() return len(infohash) == 40 and infohash.isalnum()
@ -733,6 +734,8 @@ MAGNET_SCHEME = 'magnet:?'
XT_BTIH_PARAM = 'xt=urn:btih:' XT_BTIH_PARAM = 'xt=urn:btih:'
DN_PARAM = 'dn=' DN_PARAM = 'dn='
TR_PARAM = 'tr=' TR_PARAM = 'tr='
TR_TIER_PARAM = 'tr.'
TR_TIER_REGEX = re.compile(r'^tr.(\d+)=(\S+)')
def is_magnet(uri): def is_magnet(uri):
@ -775,8 +778,6 @@ def get_magnet_info(uri):
""" """
tr0_param = 'tr.'
tr0_param_regex = re.compile(r'^tr.(\d+)=(\S+)')
if not uri.startswith(MAGNET_SCHEME): if not uri.startswith(MAGNET_SCHEME):
return {} return {}
@ -804,12 +805,14 @@ def get_magnet_info(uri):
tracker = unquote_plus(param[len(TR_PARAM) :]) tracker = unquote_plus(param[len(TR_PARAM) :])
trackers[tracker] = tier trackers[tracker] = tier
tier += 1 tier += 1
elif param.startswith(tr0_param): elif param.startswith(TR_TIER_PARAM):
try: tracker_match = re.match(TR_TIER_REGEX, param)
tier, tracker = re.match(tr0_param_regex, param).groups() if not tracker_match:
trackers[tracker] = tier continue
except AttributeError:
pass tier, tracker = tracker_match.groups()
tracker = unquote_plus(tracker)
trackers[tracker] = int(tier)
if info_hash: if info_hash:
if not name: if not name:
@ -904,6 +907,29 @@ def free_space(path):
return disk_data.f_bavail * block_size return disk_data.f_bavail * block_size
def is_interface(interface):
"""Check if interface is a valid IP or network adapter.
Args:
interface (str): The IP or interface name to test.
Returns:
bool: Whether interface is valid is not.
Examples:
Windows:
>>> is_interface('{7A30AE62-23ZA-3744-Z844-A5B042524871}')
>>> is_interface('127.0.0.1')
True
Linux:
>>> is_interface('lo')
>>> is_interface('127.0.0.1')
True
"""
return is_ip(interface) or is_interface_name(interface)
def is_ip(ip): def is_ip(ip):
"""A test to see if 'ip' is a valid IPv4 or IPv6 address. """A test to see if 'ip' is a valid IPv4 or IPv6 address.
@ -939,15 +965,12 @@ def is_ipv4(ip):
""" """
import socket
try: try:
if windows_check(): socket.inet_pton(socket.AF_INET, ip)
return socket.inet_aton(ip) except OSError:
else:
return socket.inet_pton(socket.AF_INET, ip)
except socket.error:
return False return False
else:
return True
def is_ipv6(ip): def is_ipv6(ip):
@ -966,23 +989,51 @@ def is_ipv6(ip):
""" """
try: try:
import ipaddress socket.inet_pton(socket.AF_INET6, ip)
except ImportError: except OSError:
import socket return False
try:
return socket.inet_pton(socket.AF_INET6, ip)
except (socket.error, AttributeError):
if windows_check():
log.warning('Unable to verify IPv6 Address on Windows.')
return True
else: else:
try: return True
return ipaddress.IPv6Address(decode_bytes(ip))
except ipaddress.AddressValueError:
pass
return False
def is_interface_name(name):
"""Returns True if an interface name exists.
Args:
name (str): The Interface to test. eg. eth0 linux. GUID on Windows.
Returns:
bool: Whether name is valid or not.
Examples:
>>> is_interface_name("eth0")
True
>>> is_interface_name("{7A30AE62-23ZA-3744-Z844-A5B042524871}")
True
"""
if not windows_check():
try:
socket.if_nametoindex(name)
except OSError:
pass
else:
return True
if ifaddr:
try:
adapters = ifaddr.get_adapters()
except OSError:
return True
else:
return any([name == a.name for a in adapters])
if windows_check():
regex = '^{[0-9A-Z]{8}-([0-9A-Z]{4}-){3}[0-9A-Z]{12}}$'
return bool(re.search(regex, str(name)))
return True
def decode_bytes(byte_str, encoding='utf8'): def decode_bytes(byte_str, encoding='utf8'):
@ -1060,7 +1111,7 @@ def utf8_encode_structure(data):
@functools.total_ordering @functools.total_ordering
class VersionSplit(object): class VersionSplit:
""" """
Used for comparing version numbers. Used for comparing version numbers.
@ -1239,11 +1290,7 @@ def set_env_variable(name, value):
http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py http://sourceforge.net/p/gramps/code/HEAD/tree/branches/maintenance/gramps32/src/TransUtils.py
""" """
# Update Python's copy of the environment variables # Update Python's copy of the environment variables
try: os.environ[name] = value
os.environ[name] = value
except UnicodeEncodeError:
# Python 2
os.environ[name] = value.encode('utf8')
if windows_check(): if windows_check():
from ctypes import cdll, windll from ctypes import cdll, windll
@ -1262,56 +1309,13 @@ def set_env_variable(name, value):
) )
# Update the copy maintained by msvcrt (used by gtk+ runtime) # Update the copy maintained by msvcrt (used by gtk+ runtime)
result = cdll.msvcrt._wputenv('%s=%s' % (name, value)) result = cdll.msvcrt._wputenv(f'{name}={value}')
if result != 0: if result != 0:
log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name) log.info("Failed to set Env Var '%s' (msvcrt._putenv)", name)
else: else:
log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value) log.debug("Set Env Var '%s' to '%s' (msvcrt._putenv)", name, value)
def unicode_argv():
""" Gets sys.argv as list of unicode objects on any platform."""
if windows_check():
# Versions 2.x of Python don't support Unicode in sys.argv on
# Windows, with the underlying Windows API instead replacing multi-byte
# characters with '?'.
from ctypes import POINTER, byref, c_int, cdll, windll
from ctypes.wintypes import LPCWSTR, LPWSTR
get_cmd_linew = cdll.kernel32.GetCommandLineW
get_cmd_linew.argtypes = []
get_cmd_linew.restype = LPCWSTR
cmdline_to_argvw = windll.shell32.CommandLineToArgvW
cmdline_to_argvw.argtypes = [LPCWSTR, POINTER(c_int)]
cmdline_to_argvw.restype = POINTER(LPWSTR)
cmd = get_cmd_linew()
argc = c_int(0)
argv = cmdline_to_argvw(cmd, byref(argc))
if argc.value > 0:
# Remove Python executable and commands if present
start = argc.value - len(sys.argv)
return [argv[i] for i in range(start, argc.value)]
else:
# On other platforms, we have to find the likely encoding of the args and decode
# First check if sys.stdout or stdin have encoding set
encoding = getattr(sys.stdout, 'encoding') or getattr(sys.stdin, 'encoding')
# If that fails, check what the locale is set to
encoding = encoding or locale.getpreferredencoding()
# As a last resort, just default to utf-8
encoding = encoding or 'utf-8'
arg_list = []
for arg in sys.argv:
try:
arg_list.append(arg.decode(encoding))
except AttributeError:
arg_list.append(arg)
return arg_list
def run_profiled(func, *args, **kwargs): def run_profiled(func, *args, **kwargs):
""" """
Profile a function with cProfile Profile a function with cProfile

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,13 +6,10 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import traceback import traceback
from collections import defaultdict from collections import defaultdict
from six import string_types
from twisted.internet import reactor from twisted.internet import reactor
from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
from twisted.internet.task import LoopingCall, deferLater from twisted.internet.task import LoopingCall, deferLater
@ -27,13 +23,13 @@ class ComponentAlreadyRegistered(Exception):
class ComponentException(Exception): class ComponentException(Exception):
def __init__(self, message, tb): def __init__(self, message, tb):
super(ComponentException, self).__init__(message) super().__init__(message)
self.message = message self.message = message
self.tb = tb self.tb = tb
def __str__(self): def __str__(self):
s = super(ComponentException, self).__str__() s = super().__str__()
return '%s\n%s' % (s, ''.join(self.tb)) return '{}\n{}'.format(s, ''.join(self.tb))
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
@ -45,7 +41,7 @@ class ComponentException(Exception):
return not self.__eq__(other) return not self.__eq__(other)
class Component(object): class Component:
"""Component objects are singletons managed by the :class:`ComponentRegistry`. """Component objects are singletons managed by the :class:`ComponentRegistry`.
When a new Component object is instantiated, it will be automatically When a new Component object is instantiated, it will be automatically
@ -250,7 +246,7 @@ class Component(object):
pass pass
class ComponentRegistry(object): class ComponentRegistry:
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects. """The ComponentRegistry holds a list of currently registered :class:`Component` objects.
It is used to manage the Components by starting, stopping, pausing and shutting them down. It is used to manage the Components by starting, stopping, pausing and shutting them down.
@ -325,7 +321,7 @@ class ComponentRegistry(object):
# Start all the components if names is empty # Start all the components if names is empty
if not names: if not names:
names = list(self.components) names = list(self.components)
elif isinstance(names, string_types): elif isinstance(names, str):
names = [names] names = [names]
def on_depends_started(result, name): def on_depends_started(result, name):
@ -359,7 +355,7 @@ class ComponentRegistry(object):
""" """
if not names: if not names:
names = list(self.components) names = list(self.components)
elif isinstance(names, string_types): elif isinstance(names, str):
names = [names] names = [names]
def on_dependents_stopped(result, name): def on_dependents_stopped(result, name):
@ -399,7 +395,7 @@ class ComponentRegistry(object):
""" """
if not names: if not names:
names = list(self.components) names = list(self.components)
elif isinstance(names, string_types): elif isinstance(names, str):
names = [names] names = [names]
deferreds = [] deferreds = []
@ -425,7 +421,7 @@ class ComponentRegistry(object):
""" """
if not names: if not names:
names = list(self.components) names = list(self.components)
elif isinstance(names, string_types): elif isinstance(names, str):
names = [names] names = [names]
deferreds = [] deferreds = []

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# #
@ -39,39 +38,17 @@ this can only be done for the 'config file version' and not for the 'format'
version as this will be done internally. version as this will be done internally.
""" """
from __future__ import unicode_literals
import json import json
import logging import logging
import os import os
import pickle
import shutil import shutil
from codecs import getwriter from codecs import getwriter
from io import open
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import six.moves.cPickle as pickle # noqa: N813
from deluge.common import JSON_FORMAT, get_default_config_dir from deluge.common import JSON_FORMAT, get_default_config_dir
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
callLater = None # noqa: N816 Necessary for the config tests
def prop(func):
"""Function decorator for defining property attributes
The decorated function is expected to return a dictionary
containing one or more of the following pairs:
fget - function for getting attribute value
fset - function for setting attribute value
fdel - function for deleting attribute
This can be conveniently constructed by the locals() builtin
function; see:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/205183
"""
return property(doc=func.__doc__, **func())
def find_json_objects(text, decoder=json.JSONDecoder()): def find_json_objects(text, decoder=json.JSONDecoder()):
@ -105,7 +82,22 @@ def find_json_objects(text, decoder=json.JSONDecoder()):
return objects return objects
class Config(object): def cast_to_existing_type(value, old_value):
"""Attempt to convert new value type to match old value type"""
types_match = isinstance(old_value, (type(None), type(value)))
if value is not None and not types_match:
old_type = type(old_value)
# Skip convert to bytes since requires knowledge of encoding and value should
# be unicode anyway.
if old_type is bytes:
return value
return old_type(value)
return value
class Config:
"""This class is used to access/create/modify config files. """This class is used to access/create/modify config files.
Args: Args:
@ -115,13 +107,23 @@ class Config(object):
file_version (int): The file format for the default config values when creating file_version (int): The file format for the default config values when creating
a fresh config. This value should be increased whenever a new migration function is a fresh config. This value should be increased whenever a new migration function is
setup to convert old config files. (default: 1) setup to convert old config files. (default: 1)
log_mask_funcs (dict): A dict of key:function, used to mask sensitive
key values (e.g. passwords) when logging is enabled.
""" """
def __init__(self, filename, defaults=None, config_dir=None, file_version=1): def __init__(
self,
filename,
defaults=None,
config_dir=None,
file_version=1,
log_mask_funcs=None,
):
self.__config = {} self.__config = {}
self.__set_functions = {} self.__set_functions = {}
self.__change_callbacks = [] self.__change_callbacks = []
self.__log_mask_funcs = log_mask_funcs if log_mask_funcs else {}
# These hold the version numbers and they will be set when loaded # These hold the version numbers and they will be set when loaded
self.__version = {'format': 1, 'file': file_version} self.__version = {'format': 1, 'file': file_version}
@ -132,7 +134,7 @@ class Config(object):
if defaults: if defaults:
for key, value in defaults.items(): for key, value in defaults.items():
self.set_item(key, value) self.set_item(key, value, default=True)
# Load the config from file in the config_dir # Load the config from file in the config_dir
if config_dir: if config_dir:
@ -142,6 +144,12 @@ class Config(object):
self.load() self.load()
def callLater(self, period, func, *args, **kwargs): # noqa: N802 ignore camelCase
"""Wrapper around reactor.callLater for test purpose."""
from twisted.internet import reactor
return reactor.callLater(period, func, *args, **kwargs)
def __contains__(self, item): def __contains__(self, item):
return item in self.__config return item in self.__config
@ -150,7 +158,7 @@ class Config(object):
return self.set_item(key, value) return self.set_item(key, value)
def set_item(self, key, value): def set_item(self, key, value, default=False):
"""Sets item 'key' to 'value' in the config dictionary. """Sets item 'key' to 'value' in the config dictionary.
Does not allow changing the item's type unless it is None. Does not allow changing the item's type unless it is None.
@ -162,6 +170,8 @@ class Config(object):
key (str): Item to change to change. key (str): Item to change to change.
value (any): The value to change item to, must be same type as what is value (any): The value to change item to, must be same type as what is
currently in the config. currently in the config.
default (optional, bool): When setting a default value skip func or save
callbacks.
Raises: Raises:
ValueError: Raised when the type of value is not the same as what is ValueError: Raised when the type of value is not the same as what is
@ -174,61 +184,54 @@ class Config(object):
5 5
""" """
if key not in self.__config: if isinstance(value, bytes):
self.__config[key] = value value = value.decode()
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value))
return
if self.__config[key] == value: if key in self.__config:
return
# Change the value type if it is not None and does not match.
type_match = isinstance(self.__config[key], (type(None), type(value)))
if value is not None and not type_match:
try: try:
oldtype = type(self.__config[key]) value = cast_to_existing_type(value, self.__config[key])
# Don't convert to bytes as requires encoding and value will
# be decoded anyway.
if oldtype is not bytes:
value = oldtype(value)
except ValueError: except ValueError:
log.warning('Value Type "%s" invalid for key: %s', type(value), key) log.warning('Value Type "%s" invalid for key: %s', type(value), key)
raise raise
else:
if self.__config[key] == value:
return
if isinstance(value, bytes): if log.isEnabledFor(logging.DEBUG):
value = value.decode('utf8') if key in self.__log_mask_funcs:
value = self.__log_mask_funcs[key](value)
log.debug('Setting key "%s" to: %s (of type: %s)', key, value, type(value)) log.debug(
'Setting key "%s" to: %s (of type: %s)',
key,
value,
type(value),
)
self.__config[key] = value self.__config[key] = value
global callLater # Skip save or func callbacks if setting default value for keys
if callLater is None: if default:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError return
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# Run the set_function for this key if any # Run the set_function for this key if any
try: for func in self.__set_functions.get(key, []):
for func in self.__set_functions[key]: self.callLater(0, func, key, value)
callLater(0, func, key, value)
except KeyError:
pass
try: try:
def do_change_callbacks(key, value): def do_change_callbacks(key, value):
for func in self.__change_callbacks: for func in self.__change_callbacks:
func(key, value) func(key, value)
callLater(0, do_change_callbacks, key, value) self.callLater(0, do_change_callbacks, key, value)
except Exception: except Exception:
pass pass
# We set the save_timer for 5 seconds if not already set # We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active(): if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save) self._save_timer = self.callLater(5, self.save)
def __getitem__(self, key): def __getitem__(self, key):
"""See get_item """ """See get_item"""
return self.get_item(key) return self.get_item(key)
def get_item(self, key): def get_item(self, key):
@ -301,16 +304,9 @@ class Config(object):
del self.__config[key] del self.__config[key]
global callLater
if callLater is None:
# Must import here and not at the top or it will throw ReactorAlreadyInstalledError
from twisted.internet.reactor import ( # pylint: disable=redefined-outer-name
callLater,
)
# We set the save_timer for 5 seconds if not already set # We set the save_timer for 5 seconds if not already set
if not self._save_timer or not self._save_timer.active(): if not self._save_timer or not self._save_timer.active():
self._save_timer = callLater(5, self.save) self._save_timer = self.callLater(5, self.save)
def register_change_callback(self, callback): def register_change_callback(self, callback):
"""Registers a callback function for any changed value. """Registers a callback function for any changed value.
@ -356,7 +352,6 @@ class Config(object):
# Run the function now if apply_now is set # Run the function now if apply_now is set
if apply_now: if apply_now:
function(key, self.__config[key]) function(key, self.__config[key])
return
def apply_all(self): def apply_all(self):
"""Calls all set functions. """Calls all set functions.
@ -399,9 +394,9 @@ class Config(object):
filename = self.__config_file filename = self.__config_file
try: try:
with open(filename, 'r', encoding='utf8') as _file: with open(filename, encoding='utf8') as _file:
data = _file.read() data = _file.read()
except IOError as ex: except OSError as ex:
log.warning('Unable to open config file %s: %s', filename, ex) log.warning('Unable to open config file %s: %s', filename, ex)
return return
@ -431,12 +426,24 @@ class Config(object):
log.exception(ex) log.exception(ex)
log.warning('Unable to load config file: %s', filename) log.warning('Unable to load config file: %s', filename)
if not log.isEnabledFor(logging.DEBUG):
return
config = self.__config
if self.__log_mask_funcs:
config = {
key: self.__log_mask_funcs[key](config[key])
if key in self.__log_mask_funcs
else config[key]
for key in config
}
log.debug( log.debug(
'Config %s version: %s.%s loaded: %s', 'Config %s version: %s.%s loaded: %s',
filename, filename,
self.__version['format'], self.__version['format'],
self.__version['file'], self.__version['file'],
self.__config, config,
) )
def save(self, filename=None): def save(self, filename=None):
@ -454,7 +461,7 @@ class Config(object):
# Check to see if the current config differs from the one on disk # Check to see if the current config differs from the one on disk
# We will only write a new config file if there is a difference # We will only write a new config file if there is a difference
try: try:
with open(filename, 'r', encoding='utf8') as _file: with open(filename, encoding='utf8') as _file:
data = _file.read() data = _file.read()
objects = find_json_objects(data) objects = find_json_objects(data)
start, end = objects[0] start, end = objects[0]
@ -466,7 +473,7 @@ class Config(object):
if self._save_timer and self._save_timer.active(): if self._save_timer and self._save_timer.active():
self._save_timer.cancel() self._save_timer.cancel()
return True return True
except (IOError, IndexError) as ex: except (OSError, IndexError) as ex:
log.warning('Unable to open config file: %s because: %s', filename, ex) log.warning('Unable to open config file: %s because: %s', filename, ex)
# Save the new config and make sure it's written to disk # Save the new config and make sure it's written to disk
@ -480,7 +487,7 @@ class Config(object):
json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT) json.dump(self.__config, getwriter('utf8')(_file), **JSON_FORMAT)
_file.flush() _file.flush()
os.fsync(_file.fileno()) os.fsync(_file.fileno())
except IOError as ex: except OSError as ex:
log.error('Error writing new config file: %s', ex) log.error('Error writing new config file: %s', ex)
return False return False
@ -491,7 +498,7 @@ class Config(object):
try: try:
log.debug('Backing up old config file to %s.bak', filename) log.debug('Backing up old config file to %s.bak', filename)
shutil.move(filename, filename + '.bak') shutil.move(filename, filename + '.bak')
except IOError as ex: except OSError as ex:
log.warning('Unable to backup old config: %s', ex) log.warning('Unable to backup old config: %s', ex)
# The new config file has been written successfully, so let's move it over # The new config file has been written successfully, so let's move it over
@ -499,7 +506,7 @@ class Config(object):
try: try:
log.debug('Moving new config file %s to %s', filename_tmp, filename) log.debug('Moving new config file %s to %s', filename_tmp, filename)
shutil.move(filename_tmp, filename) shutil.move(filename_tmp, filename)
except IOError as ex: except OSError as ex:
log.error('Error moving new config file: %s', ex) log.error('Error moving new config file: %s', ex)
return False return False
else: else:
@ -551,14 +558,11 @@ class Config(object):
def config_file(self): def config_file(self):
return self.__config_file return self.__config_file
@prop @property
def config(): # pylint: disable=no-method-argument def config(self):
"""The config dictionary""" """The config dictionary"""
return self.__config
def fget(self): @config.deleter
return self.__config def config(self):
return self.save()
def fdel(self):
return self.save()
return locals()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import os import os
@ -19,7 +16,7 @@ from deluge.config import Config
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class _ConfigManager(object): class _ConfigManager:
def __init__(self): def __init__(self):
log.debug('ConfigManager started..') log.debug('ConfigManager started..')
self.config_files = {} self.config_files = {}

192
deluge/conftest.py Normal file
View File

@ -0,0 +1,192 @@
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import tempfile
import warnings
from unittest.mock import Mock, patch
import pytest
import pytest_twisted
from twisted.internet import reactor
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.error import CannotListenError
from twisted.python.failure import Failure
import deluge.component as _component
import deluge.configmanager
from deluge.common import get_localhost_auth
from deluge.tests import common
from deluge.ui.client import client as _client
DEFAULT_LISTEN_PORT = 58900
@pytest.fixture
def listen_port(request):
if request and 'daemon' in request.fixturenames:
try:
return request.getfixturevalue('daemon').listen_port
except Exception:
pass
return DEFAULT_LISTEN_PORT
@pytest.fixture
def mock_callback():
"""Returns a `Mock` object which can be registered as a callback to test against.
If callback was not called within `timeout` seconds, it will raise a TimeoutError.
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
"""
def reset():
if mock.called:
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()
original_reset_mock = mock.reset_mock
mock.reset_mock = reset
mock.reset_mock()
return mock
@pytest.fixture
def config_dir(tmp_path):
deluge.configmanager.set_config_dir(tmp_path)
yield tmp_path
@pytest_twisted.async_yield_fixture()
async def client(request, config_dir, monkeypatch, listen_port):
# monkeypatch.setattr(
# _client, 'connect', functools.partial(_client.connect, port=listen_port)
# )
try:
username, password = get_localhost_auth()
except Exception:
username, password = '', ''
await _client.connect(
'localhost',
port=listen_port,
username=username,
password=password,
)
yield _client
if _client.connected():
await _client.disconnect()
@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir):
listen_port = DEFAULT_LISTEN_PORT
logfile = f'daemon_{request.node.name}.log'
if hasattr(request.cls, 'daemon_custom_script'):
custom_script = request.cls.daemon_custom_script
else:
custom_script = ''
for dummy in range(10):
try:
d, daemon = common.start_core(
listen_port=listen_port,
logfile=logfile,
timeout=5,
timeout_msg='Timeout!',
custom_script=custom_script,
print_stdout=True,
print_stderr=True,
config_directory=config_dir,
)
await d
except CannotListenError as ex:
exception_error = ex
listen_port += 1
except (KeyboardInterrupt, SystemExit):
raise
else:
break
else:
raise exception_error
daemon.listen_port = listen_port
yield daemon
await daemon.kill()
@pytest.fixture(autouse=True)
def common_fixture(config_dir, request, monkeypatch, listen_port):
"""Adds some instance attributes to test classes for backwards compatibility with old testing."""
def fail(self, reason):
if isinstance(reason, Failure):
reason = reason.value
return pytest.fail(str(reason))
if request.instance:
request.instance.patch = monkeypatch.setattr
request.instance.config_dir = config_dir
request.instance.listen_port = listen_port
request.instance.id = lambda: request.node.name
request.cls.fail = fail
@pytest_twisted.async_yield_fixture(scope='function')
async def component(request):
"""Verify component registry is clean, and clean up after test."""
if len(_component._ComponentRegistry.components) != 0:
warnings.warn(
'The component._ComponentRegistry.components is not empty on test setup.\n'
'This is probably caused by another test that did not clean up after finishing!: %s'
% _component._ComponentRegistry.components
)
yield _component
await _component.shutdown()
_component._ComponentRegistry.components.clear()
_component._ComponentRegistry.dependents.clear()
@pytest_twisted.async_yield_fixture(scope='function')
async def base_fixture(common_fixture, component, request):
"""This fixture is autoused on all tests that subclass BaseTestCase"""
self = request.instance
if hasattr(self, 'set_up'):
try:
await maybeDeferred(self.set_up)
except Exception as exc:
warnings.warn('Error caught in test setup!\n%s' % exc)
pytest.fail('Error caught in test setup!\n%s' % exc)
yield
if hasattr(self, 'tear_down'):
try:
await maybeDeferred(self.tear_down)
except Exception as exc:
pytest.fail('Error caught in test teardown!\n%s' % exc)
@pytest.mark.usefixtures('base_fixture')
class BaseTestCase:
"""This is the base class that should be used for all test classes
that create classes that inherit from deluge.component.Component. It
ensures that the component registry has been cleaned up when tests
have finished.
"""
@pytest.fixture
def mock_mkstemp(tmp_path):
"""Return known tempfile location to verify file deleted"""
tmp_file = tempfile.mkstemp(dir=tmp_path)
with patch('tempfile.mkstemp', return_value=tmp_file):
yield tmp_file

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -15,10 +14,8 @@ This should typically only be used by the Core. Plugins should utilize the
`:mod:EventManager` for similar functionality. `:mod:EventManager` for similar functionality.
""" """
from __future__ import unicode_literals
import logging import logging
import types from types import SimpleNamespace
from twisted.internet import reactor from twisted.internet import reactor
@ -28,14 +25,6 @@ from deluge.common import decode_bytes
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
try:
SimpleNamespace = types.SimpleNamespace # Python 3.3+
except AttributeError:
class SimpleNamespace(object): # Python 2.7
def __init__(self, **attr):
self.__dict__.update(attr)
class AlertManager(component.Component): class AlertManager(component.Component):
"""AlertManager fetches and processes libtorrent alerts""" """AlertManager fetches and processes libtorrent alerts"""
@ -57,6 +46,7 @@ class AlertManager(component.Component):
| lt.alert.category_t.status_notification | lt.alert.category_t.status_notification
| lt.alert.category_t.ip_block_notification | lt.alert.category_t.ip_block_notification
| lt.alert.category_t.performance_warning | lt.alert.category_t.performance_warning
| lt.alert.category_t.file_progress_notification
) )
self.session.apply_settings({'alert_mask': alert_mask}) self.session.apply_settings({'alert_mask': alert_mask})

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -8,12 +7,9 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import os import os
import shutil import shutil
from io import open
import deluge.component as component import deluge.component as component
import deluge.configmanager as configmanager import deluge.configmanager as configmanager
@ -32,14 +28,14 @@ log = logging.getLogger(__name__)
AUTH_LEVELS_MAPPING = { AUTH_LEVELS_MAPPING = {
'NONE': AUTH_LEVEL_NONE, 'NONE': AUTH_LEVEL_NONE,
'READONLY': AUTH_LEVEL_READONLY, 'READONLY': AUTH_LEVEL_READONLY,
'DEFAULT': AUTH_LEVEL_NORMAL, 'DEFAULT': AUTH_LEVEL_DEFAULT,
'NORMAL': AUTH_LEVEL_DEFAULT, 'NORMAL': AUTH_LEVEL_NORMAL,
'ADMIN': AUTH_LEVEL_ADMIN, 'ADMIN': AUTH_LEVEL_ADMIN,
} }
AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()} AUTH_LEVELS_MAPPING_REVERSE = {v: k for k, v in AUTH_LEVELS_MAPPING.items()}
class Account(object): class Account:
__slots__ = ('username', 'password', 'authlevel') __slots__ = ('username', 'password', 'authlevel')
def __init__(self, username, password, authlevel): def __init__(self, username, password, authlevel):
@ -56,10 +52,10 @@ class Account(object):
} }
def __repr__(self): def __repr__(self):
return '<Account username="%(username)s" authlevel=%(authlevel)s>' % { return '<Account username="{username}" authlevel={authlevel}>'.format(
'username': self.username, username=self.username,
'authlevel': self.authlevel, authlevel=self.authlevel,
} )
class AuthManager(component.Component): class AuthManager(component.Component):
@ -184,7 +180,7 @@ class AuthManager(component.Component):
if os.path.isfile(filepath): if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak) log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak) shutil.copy2(filepath, filepath_bak)
except IOError as ex: except OSError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else: else:
log.info('Saving the %s at: %s', filename, filepath) log.info('Saving the %s at: %s', filename, filepath)
@ -198,7 +194,7 @@ class AuthManager(component.Component):
_file.flush() _file.flush()
os.fsync(_file.fileno()) os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath) shutil.move(filepath_tmp, filepath)
except IOError as ex: except OSError as ex:
log.error('Unable to save %s: %s', filename, ex) log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak): if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak) log.info('Restoring backup of %s from: %s', filename, filepath_bak)
@ -227,9 +223,9 @@ class AuthManager(component.Component):
for _filepath in (auth_file, auth_file_bak): for _filepath in (auth_file, auth_file_bak):
log.info('Opening %s for load: %s', filename, _filepath) log.info('Opening %s for load: %s', filename, _filepath)
try: try:
with open(_filepath, 'r', encoding='utf8') as _file: with open(_filepath, encoding='utf8') as _file:
file_data = _file.readlines() file_data = _file.readlines()
except IOError as ex: except OSError as ex:
log.warning('Unable to load %s: %s', _filepath, ex) log.warning('Unable to load %s: %s', _filepath, ex)
file_data = [] file_data = []
else: else:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -8,8 +7,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import division, unicode_literals
import glob import glob
import logging import logging
import os import os
@ -17,8 +14,9 @@ import shutil
import tempfile import tempfile
import threading import threading
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.request import URLError, urlopen
from six import string_types
from twisted.internet import defer, reactor, task from twisted.internet import defer, reactor, task
from twisted.web.client import Agent, readBody from twisted.web.client import Agent, readBody
@ -41,7 +39,7 @@ from deluge.core.pluginmanager import PluginManager
from deluge.core.preferencesmanager import PreferencesManager from deluge.core.preferencesmanager import PreferencesManager
from deluge.core.rpcserver import export from deluge.core.rpcserver import export
from deluge.core.torrentmanager import TorrentManager from deluge.core.torrentmanager import TorrentManager
from deluge.decorators import deprecated from deluge.decorators import deprecated, maybe_coroutine
from deluge.error import ( from deluge.error import (
AddTorrentError, AddTorrentError,
DelugeError, DelugeError,
@ -56,12 +54,6 @@ from deluge.event import (
) )
from deluge.httpdownloader import download_file from deluge.httpdownloader import download_file
try:
from urllib.request import URLError, urlopen
except ImportError:
# PY2 fallback
from urllib2 import URLError, urlopen
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
DEPR_SESSION_STATUS_KEYS = { DEPR_SESSION_STATUS_KEYS = {
@ -120,7 +112,7 @@ class Core(component.Component):
component.Component.__init__(self, 'Core') component.Component.__init__(self, 'Core')
# Start the libtorrent session. # Start the libtorrent session.
user_agent = 'Deluge/{} libtorrent/{}'.format(DELUGE_VER, LT_VERSION) user_agent = f'Deluge/{DELUGE_VER} libtorrent/{LT_VERSION}'
peer_id = self._create_peer_id(DELUGE_VER) peer_id = self._create_peer_id(DELUGE_VER)
log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent) log.debug('Starting session (peer_id: %s, user_agent: %s)', peer_id, user_agent)
settings_pack = { settings_pack = {
@ -173,19 +165,25 @@ class Core(component.Component):
# store the one in the config so we can restore it on shutdown # store the one in the config so we can restore it on shutdown
self._old_listen_interface = None self._old_listen_interface = None
if listen_interface: if listen_interface:
if deluge.common.is_ip(listen_interface): if deluge.common.is_interface(listen_interface):
self._old_listen_interface = self.config['listen_interface'] self._old_listen_interface = self.config['listen_interface']
self.config['listen_interface'] = listen_interface self.config['listen_interface'] = listen_interface
else: else:
log.error( log.error(
'Invalid listen interface (must be IP Address): %s', 'Invalid listen interface (must be IP Address or Interface Name): %s',
listen_interface, listen_interface,
) )
self._old_outgoing_interface = None self._old_outgoing_interface = None
if outgoing_interface: if outgoing_interface:
self._old_outgoing_interface = self.config['outgoing_interface'] if deluge.common.is_interface(outgoing_interface):
self.config['outgoing_interface'] = outgoing_interface self._old_outgoing_interface = self.config['outgoing_interface']
self.config['outgoing_interface'] = outgoing_interface
else:
log.error(
'Invalid outgoing interface (must be IP Address or Interface Name): %s',
outgoing_interface,
)
# New release check information # New release check information
self.__new_release = None self.__new_release = None
@ -243,13 +241,12 @@ class Core(component.Component):
"""Apply libtorrent session settings. """Apply libtorrent session settings.
Args: Args:
settings (dict): A dict of lt session settings to apply. settings: A dict of lt session settings to apply.
""" """
self.session.apply_settings(settings) self.session.apply_settings(settings)
@staticmethod @staticmethod
def _create_peer_id(version): def _create_peer_id(version: str) -> str:
"""Create a peer_id fingerprint. """Create a peer_id fingerprint.
This creates the peer_id and modifies the release char to identify This creates the peer_id and modifies the release char to identify
@ -264,11 +261,10 @@ class Core(component.Component):
``--DE201b--`` (beta pre-release of v2.0.1) ``--DE201b--`` (beta pre-release of v2.0.1)
Args: Args:
version (str): The version string in PEP440 dotted notation. version: The version string in PEP440 dotted notation.
Returns: Returns:
str: The formatted peer_id with Deluge prefix e.g. '--DE200s--' The formatted peer_id with Deluge prefix e.g. '--DE200s--'
""" """
split = deluge.common.VersionSplit(version) split = deluge.common.VersionSplit(version)
# Fill list with zeros to length of 4 and use lt to create fingerprint. # Fill list with zeros to length of 4 and use lt to create fingerprint.
@ -301,7 +297,7 @@ class Core(component.Component):
if os.path.isfile(filepath): if os.path.isfile(filepath):
log.debug('Creating backup of %s at: %s', filename, filepath_bak) log.debug('Creating backup of %s at: %s', filename, filepath_bak)
shutil.copy2(filepath, filepath_bak) shutil.copy2(filepath, filepath_bak)
except IOError as ex: except OSError as ex:
log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex) log.error('Unable to backup %s to %s: %s', filepath, filepath_bak, ex)
else: else:
log.info('Saving the %s at: %s', filename, filepath) log.info('Saving the %s at: %s', filename, filepath)
@ -311,18 +307,17 @@ class Core(component.Component):
_file.flush() _file.flush()
os.fsync(_file.fileno()) os.fsync(_file.fileno())
shutil.move(filepath_tmp, filepath) shutil.move(filepath_tmp, filepath)
except (IOError, EOFError) as ex: except (OSError, EOFError) as ex:
log.error('Unable to save %s: %s', filename, ex) log.error('Unable to save %s: %s', filename, ex)
if os.path.isfile(filepath_bak): if os.path.isfile(filepath_bak):
log.info('Restoring backup of %s from: %s', filename, filepath_bak) log.info('Restoring backup of %s from: %s', filename, filepath_bak)
shutil.move(filepath_bak, filepath) shutil.move(filepath_bak, filepath)
def _load_session_state(self): def _load_session_state(self) -> dict:
"""Loads the libtorrent session state """Loads the libtorrent session state
Returns: Returns:
dict: A libtorrent sesion state, empty dict if unable to load it. A libtorrent sesion state, empty dict if unable to load it.
""" """
filename = 'session.state' filename = 'session.state'
filepath = get_config_dir(filename) filepath = get_config_dir(filename)
@ -333,7 +328,7 @@ class Core(component.Component):
try: try:
with open(_filepath, 'rb') as _file: with open(_filepath, 'rb') as _file:
state = lt.bdecode(_file.read()) state = lt.bdecode(_file.read())
except (IOError, EOFError, RuntimeError) as ex: except (OSError, EOFError, RuntimeError) as ex:
log.warning('Unable to load %s: %s', _filepath, ex) log.warning('Unable to load %s: %s', _filepath, ex)
else: else:
log.info('Successfully loaded %s: %s', filename, _filepath) log.info('Successfully loaded %s: %s', filename, _filepath)
@ -404,18 +399,19 @@ class Core(component.Component):
# Exported Methods # Exported Methods
@export @export
def add_torrent_file_async(self, filename, filedump, options, save_state=True): def add_torrent_file_async(
self, filename: str, filedump: str, options: dict, save_state: bool = True
) -> 'defer.Deferred[Optional[str]]':
"""Adds a torrent file to the session asynchronously. """Adds a torrent file to the session asynchronously.
Args: Args:
filename (str): The filename of the torrent. filename: The filename of the torrent.
filedump (str): A base64 encoded string of torrent file contents. filedump: A base64 encoded string of torrent file contents.
options (dict): The options to apply to the torrent upon adding. options: The options to apply to the torrent upon adding.
save_state (bool): If the state should be saved after adding the file. save_state: If the state should be saved after adding the file.
Returns: Returns:
Deferred: The torrent ID or None. The torrent ID or None.
""" """
try: try:
filedump = b64decode(filedump) filedump = b64decode(filedump)
@ -436,42 +432,39 @@ class Core(component.Component):
return d return d
@export @export
def prefetch_magnet_metadata(self, magnet, timeout=30): @maybe_coroutine
async def prefetch_magnet_metadata(
self, magnet: str, timeout: int = 30
) -> Tuple[str, bytes]:
"""Download magnet metadata without adding to Deluge session. """Download magnet metadata without adding to Deluge session.
Used by UIs to get magnet files for selection before adding to session. Used by UIs to get magnet files for selection before adding to session.
The metadata is bencoded and for transfer base64 encoded.
Args: Args:
magnet (str): The magnet URI. magnet: The magnet URI.
timeout (int): Number of seconds to wait before canceling request. timeout: Number of seconds to wait before canceling request.
Returns: Returns:
Deferred: A tuple of (torrent_id (str), metadata (dict)) for the magnet. A tuple of (torrent_id, metadata) for the magnet.
""" """
return await self.torrentmanager.prefetch_metadata(magnet, timeout)
def on_metadata(result, result_d):
"""Return result of torrent_id and metadata"""
result_d.callback(result)
return result
d = self.torrentmanager.prefetch_metadata(magnet, timeout)
# Use a separate callback chain to handle existing prefetching magnet.
result_d = defer.Deferred()
d.addBoth(on_metadata, result_d)
return result_d
@export @export
def add_torrent_file(self, filename, filedump, options): def add_torrent_file(
self, filename: str, filedump: Union[str, bytes], options: dict
) -> Optional[str]:
"""Adds a torrent file to the session. """Adds a torrent file to the session.
Args: Args:
filename (str): The filename of the torrent. filename: The filename of the torrent.
filedump (str): A base64 encoded string of the torrent file contents. filedump: A base64 encoded string of the torrent file contents.
options (dict): The options to apply to the torrent upon adding. options: The options to apply to the torrent upon adding.
Returns: Returns:
str: The torrent_id or None. The torrent_id or None.
""" """
try: try:
filedump = b64decode(filedump) filedump = b64decode(filedump)
@ -487,25 +480,26 @@ class Core(component.Component):
raise raise
@export @export
def add_torrent_files(self, torrent_files): def add_torrent_files(
self, torrent_files: List[Tuple[str, Union[str, bytes], dict]]
) -> 'defer.Deferred[List[AddTorrentError]]':
"""Adds multiple torrent files to the session asynchronously. """Adds multiple torrent files to the session asynchronously.
Args: Args:
torrent_files (list of tuples): Torrent files as tuple of torrent_files: Torrent files as tuple of
``(filename, filedump, options)``. ``(filename, filedump, options)``.
Returns: Returns:
Deferred A list of errors (if there were any)
""" """
@defer.inlineCallbacks @maybe_coroutine
def add_torrents(): async def add_torrents():
errors = [] errors = []
last_index = len(torrent_files) - 1 last_index = len(torrent_files) - 1
for idx, torrent in enumerate(torrent_files): for idx, torrent in enumerate(torrent_files):
try: try:
yield self.add_torrent_file_async( await self.add_torrent_file_async(
torrent[0], torrent[1], torrent[2], save_state=idx == last_index torrent[0], torrent[1], torrent[2], save_state=idx == last_index
) )
except AddTorrentError as ex: except AddTorrentError as ex:
@ -516,93 +510,89 @@ class Core(component.Component):
return task.deferLater(reactor, 0, add_torrents) return task.deferLater(reactor, 0, add_torrents)
@export @export
def add_torrent_url(self, url, options, headers=None): @maybe_coroutine
""" async def add_torrent_url(
Adds a torrent from a URL. Deluge will attempt to fetch the torrent self, url: str, options: dict, headers: dict = None
) -> 'defer.Deferred[Optional[str]]':
"""Adds a torrent from a URL. Deluge will attempt to fetch the torrent
from the URL prior to adding it to the session. from the URL prior to adding it to the session.
:param url: the URL pointing to the torrent file Args:
:type url: string url: the URL pointing to the torrent file
:param options: the options to apply to the torrent on add options: the options to apply to the torrent on add
:type options: dict headers: any optional headers to send
:param headers: any optional headers to send
:type headers: dict
:returns: a Deferred which returns the torrent_id as a str or None Returns:
a Deferred which returns the torrent_id as a str or None
""" """
log.info('Attempting to add URL %s', url) log.info('Attempting to add URL %s', url)
def on_download_success(filename): tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
# We got the file, so add it to the session try:
filename = await download_file(
url, tmp_file, headers=headers, force_filename=True
)
except Exception:
log.error('Failed to add torrent from URL %s', url)
raise
else:
with open(filename, 'rb') as _file: with open(filename, 'rb') as _file:
data = _file.read() data = _file.read()
try:
os.remove(filename)
except OSError as ex:
log.warning('Could not remove temp file: %s', ex)
return self.add_torrent_file(filename, b64encode(data), options) return self.add_torrent_file(filename, b64encode(data), options)
finally:
def on_download_fail(failure): try:
# Log the error and pass the failure onto the client os.close(tmp_fd)
log.error('Failed to add torrent from URL %s', url) os.remove(tmp_file)
return failure except OSError as ex:
log.warning(f'Unable to delete temp file {tmp_file}: , {ex}')
tmp_fd, tmp_file = tempfile.mkstemp(prefix='deluge_url.', suffix='.torrent')
os.close(tmp_fd)
d = download_file(url, tmp_file, headers=headers, force_filename=True)
d.addCallbacks(on_download_success, on_download_fail)
return d
@export @export
def add_torrent_magnet(self, uri, options): def add_torrent_magnet(self, uri: str, options: dict) -> str:
""" """Adds a torrent from a magnet link.
Adds a torrent from a magnet link.
:param uri: the magnet link Args:
:type uri: string uri: the magnet link
:param options: the options to apply to the torrent on add options: the options to apply to the torrent on add
:type options: dict
:returns: the torrent_id
:rtype: string
Returns:
the torrent_id
""" """
log.debug('Attempting to add by magnet URI: %s', uri) log.debug('Attempting to add by magnet URI: %s', uri)
return self.torrentmanager.add(magnet=uri, options=options) return self.torrentmanager.add(magnet=uri, options=options)
@export @export
def remove_torrent(self, torrent_id, remove_data): def remove_torrent(self, torrent_id: str, remove_data: bool) -> bool:
"""Removes a single torrent from the session. """Removes a single torrent from the session.
Args: Args:
torrent_id (str): The torrent ID to remove. torrent_id: The torrent ID to remove.
remove_data (bool): If True, also remove the downloaded data. remove_data: If True, also remove the downloaded data.
Returns: Returns:
bool: True if removed successfully. True if removed successfully.
Raises: Raises:
InvalidTorrentError: If the torrent ID does not exist in the session. InvalidTorrentError: If the torrent ID does not exist in the session.
""" """
log.debug('Removing torrent %s from the core.', torrent_id) log.debug('Removing torrent %s from the core.', torrent_id)
return self.torrentmanager.remove(torrent_id, remove_data) return self.torrentmanager.remove(torrent_id, remove_data)
@export @export
def remove_torrents(self, torrent_ids, remove_data): def remove_torrents(
self, torrent_ids: List[str], remove_data: bool
) -> 'defer.Deferred[List[Tuple[str, str]]]':
"""Remove multiple torrents from the session. """Remove multiple torrents from the session.
Args: Args:
torrent_ids (list): The torrent IDs to remove. torrent_ids: The torrent IDs to remove.
remove_data (bool): If True, also remove the downloaded data. remove_data: If True, also remove the downloaded data.
Returns: Returns:
list: An empty list if no errors occurred otherwise the list contains An empty list if no errors occurred otherwise the list contains
tuples of strings, a torrent ID and an error message. For example: tuples of strings, a torrent ID and an error message. For example:
[('<torrent_id>', 'Error removing torrent')]
[('<torrent_id>', 'Error removing torrent')]
""" """
log.info('Removing %d torrents from core.', len(torrent_ids)) log.info('Removing %d torrents from core.', len(torrent_ids))
@ -626,17 +616,17 @@ class Core(component.Component):
return task.deferLater(reactor, 0, do_remove_torrents) return task.deferLater(reactor, 0, do_remove_torrents)
@export @export
def get_session_status(self, keys): def get_session_status(self, keys: List[str]) -> Dict[str, Union[int, float]]:
"""Gets the session status values for 'keys', these keys are taking """Gets the session status values for 'keys', these keys are taking
from libtorrent's session status. from libtorrent's session status.
See: http://www.rasterbar.com/products/libtorrent/manual.html#status See: http://www.rasterbar.com/products/libtorrent/manual.html#status
:param keys: the keys for which we want values Args:
:type keys: list keys: the keys for which we want values
:returns: a dictionary of {key: value, ...}
:rtype: dict
Returns:
a dictionary of {key: value, ...}
""" """
if not keys: if not keys:
return self.session_status return self.session_status
@ -657,22 +647,22 @@ class Core(component.Component):
return status return status
@export @export
def force_reannounce(self, torrent_ids): def force_reannounce(self, torrent_ids: List[str]) -> None:
log.debug('Forcing reannouncment to: %s', torrent_ids) log.debug('Forcing reannouncment to: %s', torrent_ids)
for torrent_id in torrent_ids: for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_reannounce() self.torrentmanager[torrent_id].force_reannounce()
@export @export
def pause_torrent(self, torrent_id): def pause_torrent(self, torrent_id: str) -> None:
"""Pauses a torrent""" """Pauses a torrent"""
log.debug('Pausing: %s', torrent_id) log.debug('Pausing: %s', torrent_id)
if not isinstance(torrent_id, string_types): if not isinstance(torrent_id, str):
self.pause_torrents(torrent_id) self.pause_torrents(torrent_id)
else: else:
self.torrentmanager[torrent_id].pause() self.torrentmanager[torrent_id].pause()
@export @export
def pause_torrents(self, torrent_ids=None): def pause_torrents(self, torrent_ids: List[str] = None) -> None:
"""Pauses a list of torrents""" """Pauses a list of torrents"""
if not torrent_ids: if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list() torrent_ids = self.torrentmanager.get_torrent_list()
@ -680,27 +670,27 @@ class Core(component.Component):
self.pause_torrent(torrent_id) self.pause_torrent(torrent_id)
@export @export
def connect_peer(self, torrent_id, ip, port): def connect_peer(self, torrent_id: str, ip: str, port: int):
log.debug('adding peer %s to %s', ip, torrent_id) log.debug('adding peer %s to %s', ip, torrent_id)
if not self.torrentmanager[torrent_id].connect_peer(ip, port): if not self.torrentmanager[torrent_id].connect_peer(ip, port):
log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id) log.warning('Error adding peer %s:%s to %s', ip, port, torrent_id)
@export @export
def move_storage(self, torrent_ids, dest): def move_storage(self, torrent_ids: List[str], dest: str):
log.debug('Moving storage %s to %s', torrent_ids, dest) log.debug('Moving storage %s to %s', torrent_ids, dest)
for torrent_id in torrent_ids: for torrent_id in torrent_ids:
if not self.torrentmanager[torrent_id].move_storage(dest): if not self.torrentmanager[torrent_id].move_storage(dest):
log.warning('Error moving torrent %s to %s', torrent_id, dest) log.warning('Error moving torrent %s to %s', torrent_id, dest)
@export @export
def pause_session(self): def pause_session(self) -> None:
"""Pause the entire session""" """Pause the entire session"""
if not self.session.is_paused(): if not self.session.is_paused():
self.session.pause() self.session.pause()
component.get('EventManager').emit(SessionPausedEvent()) component.get('EventManager').emit(SessionPausedEvent())
@export @export
def resume_session(self): def resume_session(self) -> None:
"""Resume the entire session""" """Resume the entire session"""
if self.session.is_paused(): if self.session.is_paused():
self.session.resume() self.session.resume()
@ -709,21 +699,21 @@ class Core(component.Component):
component.get('EventManager').emit(SessionResumedEvent()) component.get('EventManager').emit(SessionResumedEvent())
@export @export
def is_session_paused(self): def is_session_paused(self) -> bool:
"""Returns the activity of the session""" """Returns the activity of the session"""
return self.session.is_paused() return self.session.is_paused()
@export @export
def resume_torrent(self, torrent_id): def resume_torrent(self, torrent_id: str) -> None:
"""Resumes a torrent""" """Resumes a torrent"""
log.debug('Resuming: %s', torrent_id) log.debug('Resuming: %s', torrent_id)
if not isinstance(torrent_id, string_types): if not isinstance(torrent_id, str):
self.resume_torrents(torrent_id) self.resume_torrents(torrent_id)
else: else:
self.torrentmanager[torrent_id].resume() self.torrentmanager[torrent_id].resume()
@export @export
def resume_torrents(self, torrent_ids=None): def resume_torrents(self, torrent_ids: List[str] = None) -> None:
"""Resumes a list of torrents""" """Resumes a list of torrents"""
if not torrent_ids: if not torrent_ids:
torrent_ids = self.torrentmanager.get_torrent_list() torrent_ids = self.torrentmanager.get_torrent_list()
@ -756,7 +746,9 @@ class Core(component.Component):
return status return status
@export @export
def get_torrent_status(self, torrent_id, keys, diff=False): def get_torrent_status(
self, torrent_id: str, keys: List[str], diff: bool = False
) -> dict:
torrent_keys, plugin_keys = self.torrentmanager.separate_keys( torrent_keys, plugin_keys = self.torrentmanager.separate_keys(
keys, [torrent_id] keys, [torrent_id]
) )
@ -770,57 +762,54 @@ class Core(component.Component):
) )
@export @export
def get_torrents_status(self, filter_dict, keys, diff=False): @maybe_coroutine
""" async def get_torrents_status(
returns all torrents , optionally filtered by filter_dict. self, filter_dict: dict, keys: List[str], diff: bool = False
""" ) -> dict:
"""returns all torrents , optionally filtered by filter_dict."""
all_keys = not keys
torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict) torrent_ids = self.filtermanager.filter_torrent_ids(filter_dict)
d = self.torrentmanager.torrents_status_update(torrent_ids, keys, diff=diff) status_dict, plugin_keys = await self.torrentmanager.torrents_status_update(
torrent_ids, keys, diff=diff
def add_plugin_fields(args): )
status_dict, plugin_keys = args # Ask the plugin manager to fill in the plugin keys
# Ask the plugin manager to fill in the plugin keys if len(plugin_keys) > 0 or all_keys:
if len(plugin_keys) > 0: for key in status_dict:
for key in status_dict: status_dict[key].update(self.pluginmanager.get_status(key, plugin_keys))
status_dict[key].update( return status_dict
self.pluginmanager.get_status(key, plugin_keys)
)
return status_dict
d.addCallback(add_plugin_fields)
return d
@export @export
def get_filter_tree(self, show_zero_hits=True, hide_cat=None): def get_filter_tree(
""" self, show_zero_hits: bool = True, hide_cat: List[str] = None
returns {field: [(value,count)] } ) -> Dict:
"""returns {field: [(value,count)] }
for use in sidebar(s) for use in sidebar(s)
""" """
return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat) return self.filtermanager.get_filter_tree(show_zero_hits, hide_cat)
@export @export
def get_session_state(self): def get_session_state(self) -> List[str]:
"""Returns a list of torrent_ids in the session.""" """Returns a list of torrent_ids in the session."""
# Get the torrent list from the TorrentManager # Get the torrent list from the TorrentManager
return self.torrentmanager.get_torrent_list() return self.torrentmanager.get_torrent_list()
@export @export
def get_config(self): def get_config(self) -> dict:
"""Get all the preferences as a dictionary""" """Get all the preferences as a dictionary"""
return self.config.config return self.config.config
@export @export
def get_config_value(self, key): def get_config_value(self, key: str) -> Any:
"""Get the config value for key""" """Get the config value for key"""
return self.config.get(key) return self.config.get(key)
@export @export
def get_config_values(self, keys): def get_config_values(self, keys: List[str]) -> Dict[str, Any]:
"""Get the config values for the entered keys""" """Get the config values for the entered keys"""
return {key: self.config.get(key) for key in keys} return {key: self.config.get(key) for key in keys}
@export @export
def set_config(self, config): def set_config(self, config: Dict[str, Any]):
"""Set the config with values from dictionary""" """Set the config with values from dictionary"""
# Load all the values into the configuration # Load all the values into the configuration
for key in config: for key in config:
@ -829,21 +818,20 @@ class Core(component.Component):
self.config[key] = config[key] self.config[key] = config[key]
@export @export
def get_listen_port(self): def get_listen_port(self) -> int:
"""Returns the active listen port""" """Returns the active listen port"""
return self.session.listen_port() return self.session.listen_port()
@export @export
def get_proxy(self): def get_proxy(self) -> Dict[str, Any]:
"""Returns the proxy settings """Returns the proxy settings
Returns: Returns:
dict: Contains proxy settings. Proxy settings.
Notes: Notes:
Proxy type names: Proxy type names:
0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P 0: None, 1: Socks4, 2: Socks5, 3: Socks5 w Auth, 4: HTTP, 5: HTTP w Auth, 6: I2P
""" """
settings = self.session.get_settings() settings = self.session.get_settings()
@ -866,54 +854,58 @@ class Core(component.Component):
return proxy_dict return proxy_dict
@export @export
def get_available_plugins(self): def get_available_plugins(self) -> List[str]:
"""Returns a list of plugins available in the core""" """Returns a list of plugins available in the core"""
return self.pluginmanager.get_available_plugins() return self.pluginmanager.get_available_plugins()
@export @export
def get_enabled_plugins(self): def get_enabled_plugins(self) -> List[str]:
"""Returns a list of enabled plugins in the core""" """Returns a list of enabled plugins in the core"""
return self.pluginmanager.get_enabled_plugins() return self.pluginmanager.get_enabled_plugins()
@export @export
def enable_plugin(self, plugin): def enable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
return self.pluginmanager.enable_plugin(plugin) return self.pluginmanager.enable_plugin(plugin)
@export @export
def disable_plugin(self, plugin): def disable_plugin(self, plugin: str) -> 'defer.Deferred[bool]':
return self.pluginmanager.disable_plugin(plugin) return self.pluginmanager.disable_plugin(plugin)
@export @export
def force_recheck(self, torrent_ids): def force_recheck(self, torrent_ids: List[str]) -> None:
"""Forces a data recheck on torrent_ids""" """Forces a data recheck on torrent_ids"""
for torrent_id in torrent_ids: for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].force_recheck() self.torrentmanager[torrent_id].force_recheck()
@export @export
def set_torrent_options(self, torrent_ids, options): def set_torrent_options(
self, torrent_ids: List[str], options: Dict[str, Any]
) -> None:
"""Sets the torrent options for torrent_ids """Sets the torrent options for torrent_ids
Args: Args:
torrent_ids (list): A list of torrent_ids to set the options for. torrent_ids: A list of torrent_ids to set the options for.
options (dict): A dict of torrent options to set. See options: A dict of torrent options to set. See
``torrent.TorrentOptions`` class for valid keys. ``torrent.TorrentOptions`` class for valid keys.
""" """
if 'owner' in options and not self.authmanager.has_account(options['owner']): if 'owner' in options and not self.authmanager.has_account(options['owner']):
raise DelugeError('Username "%s" is not known.' % options['owner']) raise DelugeError('Username "%s" is not known.' % options['owner'])
if isinstance(torrent_ids, string_types): if isinstance(torrent_ids, str):
torrent_ids = [torrent_ids] torrent_ids = [torrent_ids]
for torrent_id in torrent_ids: for torrent_id in torrent_ids:
self.torrentmanager[torrent_id].set_options(options) self.torrentmanager[torrent_id].set_options(options)
@export @export
def set_torrent_trackers(self, torrent_id, trackers): def set_torrent_trackers(
self, torrent_id: str, trackers: List[Dict[str, Any]]
) -> None:
"""Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``""" """Sets a torrents tracker list. trackers will be ``[{"url", "tier"}]``"""
return self.torrentmanager[torrent_id].set_trackers(trackers) return self.torrentmanager[torrent_id].set_trackers(trackers)
@export @export
def get_magnet_uri(self, torrent_id): def get_magnet_uri(self, torrent_id: str) -> str:
return self.torrentmanager[torrent_id].get_magnet_uri() return self.torrentmanager[torrent_id].get_magnet_uri()
@deprecated @deprecated
@ -1061,7 +1053,7 @@ class Core(component.Component):
self.add_torrent_file(os.path.split(target)[1], filedump, options) self.add_torrent_file(os.path.split(target)[1], filedump, options)
@export @export
def upload_plugin(self, filename, filedump): def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:
"""This method is used to upload new plugins to the daemon. It is used """This method is used to upload new plugins to the daemon. It is used
when connecting to the daemon remotely and installing a new plugin on when connecting to the daemon remotely and installing a new plugin on
the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data, the client side. ``plugin_data`` is a ``xmlrpc.Binary`` object of the file data,
@ -1079,26 +1071,24 @@ class Core(component.Component):
component.get('CorePluginManager').scan_for_plugins() component.get('CorePluginManager').scan_for_plugins()
@export @export
def rescan_plugins(self): def rescan_plugins(self) -> None:
""" """Re-scans the plugin folders for new plugins"""
Re-scans the plugin folders for new plugins
"""
component.get('CorePluginManager').scan_for_plugins() component.get('CorePluginManager').scan_for_plugins()
@export @export
def rename_files(self, torrent_id, filenames): def rename_files(
""" self, torrent_id: str, filenames: List[Tuple[int, str]]
Rename files in ``torrent_id``. Since this is an asynchronous operation by ) -> defer.Deferred:
"""Rename files in ``torrent_id``. Since this is an asynchronous operation by
libtorrent, watch for the TorrentFileRenamedEvent to know when the libtorrent, watch for the TorrentFileRenamedEvent to know when the
files have been renamed. files have been renamed.
:param torrent_id: the torrent_id to rename files Args:
:type torrent_id: string torrent_id: the torrent_id to rename files
:param filenames: a list of index, filename pairs filenames: a list of index, filename pairs
:type filenames: ((index, filename), ...)
:raises InvalidTorrentError: if torrent_id is invalid
Raises:
InvalidTorrentError: if torrent_id is invalid
""" """
if torrent_id not in self.torrentmanager.torrents: if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session') raise InvalidTorrentError('torrent_id is not in session')
@ -1109,21 +1099,20 @@ class Core(component.Component):
return task.deferLater(reactor, 0, rename) return task.deferLater(reactor, 0, rename)
@export @export
def rename_folder(self, torrent_id, folder, new_folder): def rename_folder(
""" self, torrent_id: str, folder: str, new_folder: str
Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the ) -> defer.Deferred:
"""Renames the 'folder' to 'new_folder' in 'torrent_id'. Watch for the
TorrentFolderRenamedEvent which is emitted when the folder has been TorrentFolderRenamedEvent which is emitted when the folder has been
renamed successfully. renamed successfully.
:param torrent_id: the torrent to rename folder in Args:
:type torrent_id: string torrent_id: the torrent to rename folder in
:param folder: the folder to rename folder: the folder to rename
:type folder: string new_folder: the new folder name
:param new_folder: the new folder name
:type new_folder: string
:raises InvalidTorrentError: if the torrent_id is invalid
Raises:
InvalidTorrentError: if the torrent_id is invalid
""" """
if torrent_id not in self.torrentmanager.torrents: if torrent_id not in self.torrentmanager.torrents:
raise InvalidTorrentError('torrent_id is not in session') raise InvalidTorrentError('torrent_id is not in session')
@ -1131,7 +1120,7 @@ class Core(component.Component):
return self.torrentmanager[torrent_id].rename_folder(folder, new_folder) return self.torrentmanager[torrent_id].rename_folder(folder, new_folder)
@export @export
def queue_top(self, torrent_ids): def queue_top(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to top', torrent_ids) log.debug('Attempting to queue %s to top', torrent_ids)
# torrent_ids must be sorted in reverse before moving to preserve order # torrent_ids must be sorted in reverse before moving to preserve order
for torrent_id in sorted( for torrent_id in sorted(
@ -1145,7 +1134,7 @@ class Core(component.Component):
log.warning('torrent_id: %s does not exist in the queue', torrent_id) log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export @export
def queue_up(self, torrent_ids): def queue_up(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to up', torrent_ids) log.debug('Attempting to queue %s to up', torrent_ids)
torrents = ( torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id) (self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1170,7 +1159,7 @@ class Core(component.Component):
prev_queue_position = queue_position prev_queue_position = queue_position
@export @export
def queue_down(self, torrent_ids): def queue_down(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to down', torrent_ids) log.debug('Attempting to queue %s to down', torrent_ids)
torrents = ( torrents = (
(self.torrentmanager.get_queue_position(torrent_id), torrent_id) (self.torrentmanager.get_queue_position(torrent_id), torrent_id)
@ -1195,7 +1184,7 @@ class Core(component.Component):
prev_queue_position = queue_position prev_queue_position = queue_position
@export @export
def queue_bottom(self, torrent_ids): def queue_bottom(self, torrent_ids: List[str]) -> None:
log.debug('Attempting to queue %s to bottom', torrent_ids) log.debug('Attempting to queue %s to bottom', torrent_ids)
# torrent_ids must be sorted before moving to preserve order # torrent_ids must be sorted before moving to preserve order
for torrent_id in sorted( for torrent_id in sorted(
@ -1209,17 +1198,15 @@ class Core(component.Component):
log.warning('torrent_id: %s does not exist in the queue', torrent_id) log.warning('torrent_id: %s does not exist in the queue', torrent_id)
@export @export
def glob(self, path): def glob(self, path: str) -> List[str]:
return glob.glob(path) return glob.glob(path)
@export @export
def test_listen_port(self): def test_listen_port(self) -> 'defer.Deferred[Optional[bool]]':
""" """Checks if the active port is open
Checks if the active port is open
:returns: True if the port is open, False if not
:rtype: bool
Returns:
True if the port is open, False if not
""" """
port = self.get_listen_port() port = self.get_listen_port()
url = 'https://deluge-torrent.org/test_port.php?port=%s' % port url = 'https://deluge-torrent.org/test_port.php?port=%s' % port
@ -1238,18 +1225,17 @@ class Core(component.Component):
return d return d
@export @export
def get_free_space(self, path=None): def get_free_space(self, path: str = None) -> int:
""" """Returns the number of free bytes at path
Returns the number of free bytes at path
:param path: the path to check free space at, if None, use the default download location Args:
:type path: string path: the path to check free space at, if None, use the default download location
:returns: the number of free bytes at path Returns:
:rtype: int the number of free bytes at path
:raises InvalidPathError: if the path is invalid
Raises:
InvalidPathError: if the path is invalid
""" """
if not path: if not path:
path = self.config['download_location'] path = self.config['download_location']
@ -1262,46 +1248,40 @@ class Core(component.Component):
self.external_ip = external_ip self.external_ip = external_ip
@export @export
def get_external_ip(self): def get_external_ip(self) -> str:
""" """Returns the external IP address received from libtorrent."""
Returns the external IP address received from libtorrent.
"""
return self.external_ip return self.external_ip
@export @export
def get_libtorrent_version(self): def get_libtorrent_version(self) -> str:
""" """Returns the libtorrent version.
Returns the libtorrent version.
:returns: the version
:rtype: string
Returns:
the version
""" """
return LT_VERSION return LT_VERSION
@export @export
def get_completion_paths(self, args): def get_completion_paths(self, args: Dict[str, Any]) -> Dict[str, Any]:
""" """Returns the available path completions for the input value."""
Returns the available path completions for the input value.
"""
return path_chooser_common.get_completion_paths(args) return path_chooser_common.get_completion_paths(args)
@export(AUTH_LEVEL_ADMIN) @export(AUTH_LEVEL_ADMIN)
def get_known_accounts(self): def get_known_accounts(self) -> List[Dict[str, Any]]:
return self.authmanager.get_known_accounts() return self.authmanager.get_known_accounts()
@export(AUTH_LEVEL_NONE) @export(AUTH_LEVEL_NONE)
def get_auth_levels_mappings(self): def get_auth_levels_mappings(self) -> Tuple[Dict[str, int], Dict[int, str]]:
return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE) return (AUTH_LEVELS_MAPPING, AUTH_LEVELS_MAPPING_REVERSE)
@export(AUTH_LEVEL_ADMIN) @export(AUTH_LEVEL_ADMIN)
def create_account(self, username, password, authlevel): def create_account(self, username: str, password: str, authlevel: str) -> bool:
return self.authmanager.create_account(username, password, authlevel) return self.authmanager.create_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN) @export(AUTH_LEVEL_ADMIN)
def update_account(self, username, password, authlevel): def update_account(self, username: str, password: str, authlevel: str) -> bool:
return self.authmanager.update_account(username, password, authlevel) return self.authmanager.update_account(username, password, authlevel)
@export(AUTH_LEVEL_ADMIN) @export(AUTH_LEVEL_ADMIN)
def remove_account(self, username): def remove_account(self, username: str) -> bool:
return self.authmanager.remove_account(username) return self.authmanager.remove_account(username)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -8,8 +7,6 @@
# #
"""The Deluge daemon""" """The Deluge daemon"""
from __future__ import unicode_literals
import logging import logging
import os import os
import socket import socket
@ -44,8 +41,8 @@ def is_daemon_running(pid_file):
try: try:
with open(pid_file) as _file: with open(pid_file) as _file:
pid, port = [int(x) for x in _file.readline().strip().split(';')] pid, port = (int(x) for x in _file.readline().strip().split(';'))
except (EnvironmentError, ValueError): except (OSError, ValueError):
return False return False
if is_process_running(pid): if is_process_running(pid):
@ -53,7 +50,7 @@ def is_daemon_running(pid_file):
_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try: try:
_socket.connect(('127.0.0.1', port)) _socket.connect(('127.0.0.1', port))
except socket.error: except OSError:
# Can't connect, so pid is not a deluged process. # Can't connect, so pid is not a deluged process.
return False return False
else: else:
@ -62,7 +59,7 @@ def is_daemon_running(pid_file):
return True return True
class Daemon(object): class Daemon:
"""The Deluge Daemon class""" """The Deluge Daemon class"""
def __init__( def __init__(
@ -156,7 +153,7 @@ class Daemon(object):
pid = os.getpid() pid = os.getpid()
log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file) log.debug('Storing pid %s & port %s in: %s', pid, self.port, self.pid_file)
with open(self.pid_file, 'w') as _file: with open(self.pid_file, 'w') as _file:
_file.write('%s;%s\n' % (pid, self.port)) _file.write(f'{pid};{self.port}\n')
component.start() component.start()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
@ -7,8 +6,6 @@
# the additional special exception to link portions of this program with the OpenSSL library. # the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import print_function, unicode_literals
import os import os
import sys import sys
from logging import DEBUG, FileHandler, getLogger from logging import DEBUG, FileHandler, getLogger

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import deluge.component as component import deluge.component as component

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -7,12 +6,8 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from six import string_types
import deluge.component as component import deluge.component as component
from deluge.common import TORRENT_STATE from deluge.common import TORRENT_STATE
@ -136,7 +131,7 @@ class FilterManager(component.Component):
# Sanitize input: filter-value must be a list of strings # Sanitize input: filter-value must be a list of strings
for key, value in filter_dict.items(): for key, value in filter_dict.items():
if isinstance(value, string_types): if isinstance(value, str):
filter_dict[key] = [value] filter_dict[key] = [value]
# Optimized filter for id # Optimized filter for id

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# #
@ -9,8 +8,6 @@
"""PluginManager for Core""" """PluginManager for Core"""
from __future__ import unicode_literals
import logging import logging
from twisted.internet import defer from twisted.internet import defer

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008-2010 Andrew Resch <andrewresch@gmail.com>
# #
@ -8,13 +7,13 @@
# #
from __future__ import unicode_literals
import logging import logging
import os import os
import platform import platform
import random import random
import threading import threading
from urllib.parse import quote_plus
from urllib.request import urlopen
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
@ -24,17 +23,14 @@ import deluge.configmanager
from deluge._libtorrent import lt from deluge._libtorrent import lt
from deluge.event import ConfigValueChangedEvent from deluge.event import ConfigValueChangedEvent
GeoIP = None
try: try:
import GeoIP from GeoIP import GeoIP
except ImportError: except ImportError:
GeoIP = None try:
from pygeoip import GeoIP
try: except ImportError:
from urllib.parse import quote_plus pass
from urllib.request import urlopen
except ImportError:
from urllib import quote_plus
from urllib2 import urlopen
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -202,7 +198,7 @@ class PreferencesManager(component.Component):
self.__set_listen_on() self.__set_listen_on()
def __set_listen_on(self): def __set_listen_on(self):
""" Set the ports and interface address to listen for incoming connections on.""" """Set the ports and interface address to listen for incoming connections on."""
if self.config['random_port']: if self.config['random_port']:
if not self.config['listen_random_port']: if not self.config['listen_random_port']:
self.config['listen_random_port'] = random.randrange(49152, 65525) self.config['listen_random_port'] = random.randrange(49152, 65525)
@ -225,7 +221,7 @@ class PreferencesManager(component.Component):
self.config['listen_use_sys_port'], self.config['listen_use_sys_port'],
) )
interfaces = [ interfaces = [
'%s:%s' % (interface, port) f'{interface}:{port}'
for port in range(listen_ports[0], listen_ports[1] + 1) for port in range(listen_ports[0], listen_ports[1] + 1)
] ]
self.core.apply_session_settings( self.core.apply_session_settings(
@ -400,7 +396,7 @@ class PreferencesManager(component.Component):
+ quote_plus(':'.join(self.config['enabled_plugins'])) + quote_plus(':'.join(self.config['enabled_plugins']))
) )
urlopen(url) urlopen(url)
except IOError as ex: except OSError as ex:
log.debug('Network error while trying to send info: %s', ex) log.debug('Network error while trying to send info: %s', ex)
else: else:
self.config['info_sent'] = now self.config['info_sent'] = now
@ -464,11 +460,9 @@ class PreferencesManager(component.Component):
# Load the GeoIP DB for country look-ups if available # Load the GeoIP DB for country look-ups if available
if os.path.exists(geoipdb_path): if os.path.exists(geoipdb_path):
try: try:
self.core.geoip_instance = GeoIP.open( self.core.geoip_instance = GeoIP(geoipdb_path, 0)
geoipdb_path, GeoIP.GEOIP_STANDARD except Exception as ex:
) log.warning('GeoIP Unavailable: %s', ex)
except AttributeError:
log.warning('GeoIP Unavailable')
else: else:
log.warning('Unable to find GeoIP database file: %s', geoipdb_path) log.warning('Unable to find GeoIP database file: %s', geoipdb_path)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008,2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -8,17 +7,14 @@
# #
"""RPCServer Module""" """RPCServer Module"""
from __future__ import unicode_literals
import logging import logging
import os import os
import stat
import sys import sys
import traceback import traceback
from collections import namedtuple from collections import namedtuple
from types import FunctionType from types import FunctionType
from typing import Callable, TypeVar, overload
from OpenSSL import crypto
from twisted.internet import defer, reactor from twisted.internet import defer, reactor
from twisted.internet.protocol import Factory, connectionDone from twisted.internet.protocol import Factory, connectionDone
@ -29,7 +25,7 @@ from deluge.core.authmanager import (
AUTH_LEVEL_DEFAULT, AUTH_LEVEL_DEFAULT,
AUTH_LEVEL_NONE, AUTH_LEVEL_NONE,
) )
from deluge.crypto_utils import get_context_factory from deluge.crypto_utils import check_ssl_keys, get_context_factory
from deluge.error import ( from deluge.error import (
DelugeError, DelugeError,
IncompatibleClient, IncompatibleClient,
@ -46,6 +42,18 @@ RPC_EVENT = 3
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
TCallable = TypeVar('TCallable', bound=Callable)
@overload
def export(func: TCallable) -> TCallable:
...
@overload
def export(auth_level: int) -> Callable[[TCallable], TCallable]:
...
def export(auth_level=AUTH_LEVEL_DEFAULT): def export(auth_level=AUTH_LEVEL_DEFAULT):
""" """
@ -69,7 +77,7 @@ def export(auth_level=AUTH_LEVEL_DEFAULT):
if func.__doc__: if func.__doc__:
if func.__doc__.endswith(' '): if func.__doc__.endswith(' '):
indent = func.__doc__.split('\n')[-1] indent = func.__doc__.split('\n')[-1]
func.__doc__ += '\n{}'.format(indent) func.__doc__ += f'\n{indent}'
else: else:
func.__doc__ += '\n\n' func.__doc__ += '\n\n'
func.__doc__ += rpc_text func.__doc__ += rpc_text
@ -114,7 +122,7 @@ def format_request(call):
class DelugeRPCProtocol(DelugeTransferProtocol): class DelugeRPCProtocol(DelugeTransferProtocol):
def __init__(self): def __init__(self):
super(DelugeRPCProtocol, self).__init__() super().__init__()
# namedtuple subclass with auth_level, username for the connected session. # namedtuple subclass with auth_level, username for the connected session.
self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username') self.AuthLevel = namedtuple('SessionAuthlevel', 'auth_level, username')
@ -588,59 +596,3 @@ class RPCServer(component.Component):
def stop(self): def stop(self):
self.factory.state = 'stopping' self.factory.state = 'stopping'
def check_ssl_keys():
"""
Check for SSL cert/key and create them if necessary
"""
ssl_dir = deluge.configmanager.get_config_dir('ssl')
if not os.path.exists(ssl_dir):
# The ssl folder doesn't exist so we need to create it
os.makedirs(ssl_dir)
generate_ssl_keys()
else:
for f in ('daemon.pkey', 'daemon.cert'):
if not os.path.exists(os.path.join(ssl_dir, f)):
generate_ssl_keys()
break
def generate_ssl_keys():
"""
This method generates a new SSL key/cert.
"""
from deluge.common import PY2
digest = 'sha256' if not PY2 else b'sha256'
# Generate key pair
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
# Generate cert request
req = crypto.X509Req()
subj = req.get_subject()
setattr(subj, 'CN', 'Deluge Daemon')
req.set_pubkey(pkey)
req.sign(pkey, digest)
# Generate certificate
cert = crypto.X509()
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, digest)
# Write out files
ssl_dir = deluge.configmanager.get_config_dir('ssl')
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
# Make the files only readable by this user
for f in ('daemon.pkey', 'daemon.cert'):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -14,11 +13,12 @@ Attributes:
""" """
from __future__ import division, unicode_literals
import logging import logging
import os import os
import socket import socket
import time
from typing import Optional
from urllib.parse import urlparse
from twisted.internet.defer import Deferred, DeferredList from twisted.internet.defer import Deferred, DeferredList
@ -34,18 +34,6 @@ from deluge.event import (
TorrentTrackerStatusEvent, TorrentTrackerStatusEvent,
) )
try:
from urllib.parse import urlparse
except ImportError:
# PY2 fallback
from urlparse import urlparse # pylint: disable=ungrouped-imports
try:
from future_builtins import zip
except ImportError:
# Ignore on Py3.
pass
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
LT_TORRENT_STATE_MAP = { LT_TORRENT_STATE_MAP = {
@ -94,7 +82,7 @@ def convert_lt_files(files):
"""Indexes and decodes files from libtorrent get_files(). """Indexes and decodes files from libtorrent get_files().
Args: Args:
files (list): The libtorrent torrent files. files (file_storage): The libtorrent torrent files.
Returns: Returns:
list of dict: The files. list of dict: The files.
@ -109,18 +97,18 @@ def convert_lt_files(files):
} }
""" """
filelist = [] filelist = []
for index, _file in enumerate(files): for index in range(files.num_files()):
try: try:
file_path = _file.path.decode('utf8') file_path = files.file_path(index).decode('utf8')
except AttributeError: except AttributeError:
file_path = _file.path file_path = files.file_path(index)
filelist.append( filelist.append(
{ {
'index': index, 'index': index,
'path': file_path.replace('\\', '/'), 'path': file_path.replace('\\', '/'),
'size': _file.size, 'size': files.file_size(index),
'offset': _file.offset, 'offset': files.file_offset(index),
} }
) )
@ -161,7 +149,7 @@ class TorrentOptions(dict):
""" """
def __init__(self): def __init__(self):
super(TorrentOptions, self).__init__() super().__init__()
config = ConfigManager('core.conf').config config = ConfigManager('core.conf').config
options_conf_map = { options_conf_map = {
'add_paused': 'add_paused', 'add_paused': 'add_paused',
@ -191,14 +179,14 @@ class TorrentOptions(dict):
self['seed_mode'] = False self['seed_mode'] = False
class TorrentError(object): class TorrentError:
def __init__(self, error_message, was_paused=False, restart_to_resume=False): def __init__(self, error_message, was_paused=False, restart_to_resume=False):
self.error_message = error_message self.error_message = error_message
self.was_paused = was_paused self.was_paused = was_paused
self.restart_to_resume = restart_to_resume self.restart_to_resume = restart_to_resume
class Torrent(object): class Torrent:
"""Torrent holds information about torrents added to the libtorrent session. """Torrent holds information about torrents added to the libtorrent session.
Args: Args:
@ -248,9 +236,10 @@ class Torrent(object):
self.handle = handle self.handle = handle
self.magnet = magnet self.magnet = magnet
self.status = self.handle.status() self._status: Optional['lt.torrent_status'] = None
self._status_last_update: float = 0.0
self.torrent_info = self.handle.get_torrent_info() self.torrent_info = self.handle.torrent_file()
self.has_metadata = self.status.has_metadata self.has_metadata = self.status.has_metadata
self.options = TorrentOptions() self.options = TorrentOptions()
@ -281,7 +270,6 @@ class Torrent(object):
self.prev_status = {} self.prev_status = {}
self.waiting_on_folder_rename = [] self.waiting_on_folder_rename = []
self.update_status(self.handle.status())
self._create_status_funcs() self._create_status_funcs()
self.set_options(self.options) self.set_options(self.options)
self.update_state() self.update_state()
@ -289,6 +277,18 @@ class Torrent(object):
if log.isEnabledFor(logging.DEBUG): if log.isEnabledFor(logging.DEBUG):
log.debug('Torrent object created.') log.debug('Torrent object created.')
def _set_handle_flags(self, flag: lt.torrent_flags, set_flag: bool):
"""set or unset a flag to the lt handle
Args:
flag (lt.torrent_flags): the flag to set/unset
set_flag (bool): True for setting the flag, False for unsetting it
"""
if set_flag:
self.handle.set_flags(flag)
else:
self.handle.unset_flags(flag)
def on_metadata_received(self): def on_metadata_received(self):
"""Process the metadata received alert for this torrent""" """Process the metadata received alert for this torrent"""
self.has_metadata = True self.has_metadata = True
@ -373,7 +373,7 @@ class Torrent(object):
"""Sets maximum download speed for this torrent. """Sets maximum download speed for this torrent.
Args: Args:
m_up_speed (float): Maximum download speed in KiB/s. m_down_speed (float): Maximum download speed in KiB/s.
""" """
self.options['max_download_speed'] = m_down_speed self.options['max_download_speed'] = m_down_speed
if m_down_speed < 0: if m_down_speed < 0:
@ -405,7 +405,7 @@ class Torrent(object):
return return
# A list of priorities for each piece in the torrent # A list of priorities for each piece in the torrent
priorities = self.handle.piece_priorities() priorities = self.handle.get_piece_priorities()
def get_file_piece(idx, byte_offset): def get_file_piece(idx, byte_offset):
return self.torrent_info.map_file(idx, byte_offset, 0).piece return self.torrent_info.map_file(idx, byte_offset, 0).piece
@ -438,7 +438,10 @@ class Torrent(object):
sequential (bool): Enable sequential downloading. sequential (bool): Enable sequential downloading.
""" """
self.options['sequential_download'] = sequential self.options['sequential_download'] = sequential
self.handle.set_sequential_download(sequential) self._set_handle_flags(
flag=lt.torrent_flags.sequential_download,
set_flag=sequential,
)
def set_auto_managed(self, auto_managed): def set_auto_managed(self, auto_managed):
"""Set auto managed mode, i.e. will be started or queued automatically. """Set auto managed mode, i.e. will be started or queued automatically.
@ -448,7 +451,10 @@ class Torrent(object):
""" """
self.options['auto_managed'] = auto_managed self.options['auto_managed'] = auto_managed
if not (self.status.paused and not self.status.auto_managed): if not (self.status.paused and not self.status.auto_managed):
self.handle.auto_managed(auto_managed) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=auto_managed,
)
self.update_state() self.update_state()
def set_super_seeding(self, super_seeding): def set_super_seeding(self, super_seeding):
@ -458,7 +464,10 @@ class Torrent(object):
super_seeding (bool): Enable super seeding. super_seeding (bool): Enable super seeding.
""" """
self.options['super_seeding'] = super_seeding self.options['super_seeding'] = super_seeding
self.handle.super_seeding(super_seeding) self._set_handle_flags(
flag=lt.torrent_flags.super_seeding,
set_flag=super_seeding,
)
def set_stop_ratio(self, stop_ratio): def set_stop_ratio(self, stop_ratio):
"""The seeding ratio to stop (or remove) the torrent at. """The seeding ratio to stop (or remove) the torrent at.
@ -519,7 +528,7 @@ class Torrent(object):
self.handle.prioritize_files(file_priorities) self.handle.prioritize_files(file_priorities)
else: else:
log.debug('Unable to set new file priorities.') log.debug('Unable to set new file priorities.')
file_priorities = self.handle.file_priorities() file_priorities = self.handle.get_file_priorities()
if 0 in self.options['file_priorities']: if 0 in self.options['file_priorities']:
# Previously marked a file 'skip' so check for any 0's now >0. # Previously marked a file 'skip' so check for any 0's now >0.
@ -569,7 +578,7 @@ class Torrent(object):
trackers (list of dicts): A list of trackers. trackers (list of dicts): A list of trackers.
""" """
if trackers is None: if trackers is None:
self.trackers = [tracker for tracker in self.handle.trackers()] self.trackers = list(self.handle.trackers())
self.tracker_host = None self.tracker_host = None
return return
@ -634,7 +643,7 @@ class Torrent(object):
def update_state(self): def update_state(self):
"""Updates the state, based on libtorrent's torrent state""" """Updates the state, based on libtorrent's torrent state"""
status = self.handle.status() status = self.get_lt_status()
session_paused = component.get('Core').session.is_paused() session_paused = component.get('Core').session.is_paused()
old_state = self.state old_state = self.state
self.set_status_message() self.set_status_message()
@ -646,7 +655,10 @@ class Torrent(object):
elif status_error: elif status_error:
self.state = 'Error' self.state = 'Error'
# auto-manage status will be reverted upon resuming. # auto-manage status will be reverted upon resuming.
self.handle.auto_managed(False) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.set_status_message(decode_bytes(status_error)) self.set_status_message(decode_bytes(status_error))
elif status.moving_storage: elif status.moving_storage:
self.state = 'Moving' self.state = 'Moving'
@ -699,8 +711,11 @@ class Torrent(object):
restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting restart_to_resume (bool, optional): Prevent resuming clearing the error, only restarting
session can resume. session can resume.
""" """
status = self.handle.status() status = self.get_lt_status()
self.handle.auto_managed(False) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
self.forced_error = TorrentError(message, status.paused, restart_to_resume) self.forced_error = TorrentError(message, status.paused, restart_to_resume)
if not status.paused: if not status.paused:
self.handle.pause() self.handle.pause()
@ -714,7 +729,10 @@ class Torrent(object):
log.error('Restart deluge to clear this torrent error') log.error('Restart deluge to clear this torrent error')
if not self.forced_error.was_paused and self.options['auto_managed']: if not self.forced_error.was_paused and self.options['auto_managed']:
self.handle.auto_managed(True) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
self.forced_error = None self.forced_error = None
self.set_status_message('OK') self.set_status_message('OK')
if update_state: if update_state:
@ -838,7 +856,7 @@ class Torrent(object):
'client': client, 'client': client,
'country': country, 'country': country,
'down_speed': peer.payload_down_speed, 'down_speed': peer.payload_down_speed,
'ip': '%s:%s' % (peer.ip[0], peer.ip[1]), 'ip': f'{peer.ip[0]}:{peer.ip[1]}',
'progress': peer.progress, 'progress': peer.progress,
'seed': peer.flags & peer.seed, 'seed': peer.flags & peer.seed,
'up_speed': peer.payload_up_speed, 'up_speed': peer.payload_up_speed,
@ -857,7 +875,7 @@ class Torrent(object):
def get_file_priorities(self): def get_file_priorities(self):
"""Return the file priorities""" """Return the file priorities"""
if not self.handle.has_metadata(): if not self.handle.status().has_metadata:
return [] return []
if not self.options['file_priorities']: if not self.options['file_priorities']:
@ -910,7 +928,7 @@ class Torrent(object):
# Check if hostname is an IP address and just return it if that's the case # Check if hostname is an IP address and just return it if that's the case
try: try:
socket.inet_aton(host) socket.inet_aton(host)
except socket.error: except OSError:
pass pass
else: else:
# This is an IP address because an exception wasn't raised # This is an IP address because an exception wasn't raised
@ -946,10 +964,10 @@ class Torrent(object):
if self.has_metadata: if self.has_metadata:
# Use the top-level folder as torrent name. # Use the top-level folder as torrent name.
filename = decode_bytes(self.torrent_info.file_at(0).path) filename = decode_bytes(self.torrent_info.files().file_path(0))
name = filename.replace('\\', '/', 1).split('/', 1)[0] name = filename.replace('\\', '/', 1).split('/', 1)[0]
else: else:
name = decode_bytes(self.handle.name()) name = decode_bytes(self.handle.status().name)
if not name: if not name:
name = self.torrent_id name = self.torrent_id
@ -1008,7 +1026,7 @@ class Torrent(object):
dict: a dictionary of the status keys and their values dict: a dictionary of the status keys and their values
""" """
if update: if update:
self.update_status(self.handle.status()) self.get_lt_status()
if all_keys: if all_keys:
keys = list(self.status_funcs) keys = list(self.status_funcs)
@ -1038,13 +1056,35 @@ class Torrent(object):
return status_dict return status_dict
def update_status(self, status): def get_lt_status(self) -> 'lt.torrent_status':
"""Get the torrent status fresh, not from cache.
This should be used when a guaranteed fresh status is needed rather than
`torrent.handle.status()` because it will update the cache as well.
"""
self.status = self.handle.status()
return self.status
@property
def status(self) -> 'lt.torrent_status':
"""Cached copy of the libtorrent status for this torrent.
If it has not been updated within the last five seconds, it will be
automatically refreshed.
"""
if self._status_last_update < (time.time() - 5):
self.status = self.handle.status()
return self._status
@status.setter
def status(self, status: 'lt.torrent_status') -> None:
"""Updates the cached status. """Updates the cached status.
Args: Args:
status (libtorrent.torrent_status): a libtorrent torrent status status: a libtorrent torrent status
""" """
self.status = status self._status = status
self._status_last_update = time.time()
def _create_status_funcs(self): def _create_status_funcs(self):
"""Creates the functions for getting torrent status""" """Creates the functions for getting torrent status"""
@ -1166,7 +1206,10 @@ class Torrent(object):
""" """
# Turn off auto-management so the torrent will not be unpaused by lt queueing # Turn off auto-management so the torrent will not be unpaused by lt queueing
self.handle.auto_managed(False) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=False,
)
if self.state == 'Error': if self.state == 'Error':
log.debug('Unable to pause torrent while in Error state') log.debug('Unable to pause torrent while in Error state')
elif self.status.paused: elif self.status.paused:
@ -1201,7 +1244,10 @@ class Torrent(object):
else: else:
# Check if torrent was originally being auto-managed. # Check if torrent was originally being auto-managed.
if self.options['auto_managed']: if self.options['auto_managed']:
self.handle.auto_managed(True) self._set_handle_flags(
flag=lt.torrent_flags.auto_managed,
set_flag=True,
)
try: try:
self.handle.resume() self.handle.resume()
except RuntimeError as ex: except RuntimeError as ex:
@ -1305,7 +1351,7 @@ class Torrent(object):
try: try:
with open(filepath, 'wb') as save_file: with open(filepath, 'wb') as save_file:
save_file.write(filedump) save_file.write(filedump)
except IOError as ex: except OSError as ex:
log.error('Unable to save torrent file to: %s', ex) log.error('Unable to save torrent file to: %s', ex)
filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent') filepath = os.path.join(get_config_dir(), 'state', self.torrent_id + '.torrent')

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -8,25 +7,23 @@
# #
"""TorrentManager handles Torrent objects""" """TorrentManager handles Torrent objects"""
from __future__ import unicode_literals
import datetime import datetime
import logging import logging
import operator import operator
import os import os
import pickle
import time import time
from collections import namedtuple from base64 import b64encode
from tempfile import gettempdir from tempfile import gettempdir
from typing import Dict, List, NamedTuple, Tuple
import six.moves.cPickle as pickle # noqa: N813 from twisted.internet import defer, reactor, threads
from twisted.internet import defer, error, reactor, threads
from twisted.internet.defer import Deferred, DeferredList from twisted.internet.defer import Deferred, DeferredList
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
import deluge.component as component import deluge.component as component
from deluge._libtorrent import LT_VERSION, lt from deluge._libtorrent import LT_VERSION, lt
from deluge.common import ( from deluge.common import (
PY2,
VersionSplit, VersionSplit,
archive_files, archive_files,
decode_bytes, decode_bytes,
@ -36,6 +33,7 @@ from deluge.common import (
from deluge.configmanager import ConfigManager, get_config_dir from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.authmanager import AUTH_LEVEL_ADMIN from deluge.core.authmanager import AUTH_LEVEL_ADMIN
from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath from deluge.core.torrent import Torrent, TorrentOptions, sanitize_filepath
from deluge.decorators import maybe_coroutine
from deluge.error import AddTorrentError, InvalidTorrentError from deluge.error import AddTorrentError, InvalidTorrentError
from deluge.event import ( from deluge.event import (
ExternalIPEvent, ExternalIPEvent,
@ -59,6 +57,11 @@ LT_DEFAULT_ADD_TORRENT_FLAGS = (
) )
class PrefetchQueueItem(NamedTuple):
alert_deferred: Deferred
result_queue: List[Deferred]
class TorrentState: # pylint: disable=old-style-class class TorrentState: # pylint: disable=old-style-class
"""Create a torrent state. """Create a torrent state.
@ -136,7 +139,8 @@ class TorrentManager(component.Component):
""" """
callLater = reactor.callLater # noqa: N815 # This is used in the test to mock out timeouts
clock = reactor
def __init__(self): def __init__(self):
component.Component.__init__( component.Component.__init__(
@ -165,7 +169,7 @@ class TorrentManager(component.Component):
self.is_saving_state = False self.is_saving_state = False
self.save_resume_data_file_lock = defer.DeferredLock() self.save_resume_data_file_lock = defer.DeferredLock()
self.torrents_loading = {} self.torrents_loading = {}
self.prefetching_metadata = {} self.prefetching_metadata: Dict[str, PrefetchQueueItem] = {}
# This is a map of torrent_ids to Deferreds used to track needed resume data. # This is a map of torrent_ids to Deferreds used to track needed resume data.
# The Deferreds will be completed when resume data has been saved. # The Deferreds will be completed when resume data has been saved.
@ -250,8 +254,8 @@ class TorrentManager(component.Component):
self.save_resume_data_timer.start(190, False) self.save_resume_data_timer.start(190, False)
self.prev_status_cleanup_loop.start(10) self.prev_status_cleanup_loop.start(10)
@defer.inlineCallbacks @maybe_coroutine
def stop(self): async def stop(self):
# Stop timers # Stop timers
if self.save_state_timer.running: if self.save_state_timer.running:
self.save_state_timer.stop() self.save_state_timer.stop()
@ -263,11 +267,11 @@ class TorrentManager(component.Component):
self.prev_status_cleanup_loop.stop() self.prev_status_cleanup_loop.stop()
# Save state on shutdown # Save state on shutdown
yield self.save_state() await self.save_state()
self.session.pause() self.session.pause()
result = yield self.save_resume_data(flush_disk_cache=True) result = await self.save_resume_data(flush_disk_cache=True)
# Remove the temp_file to signify successfully saved state # Remove the temp_file to signify successfully saved state
if result and os.path.isfile(self.temp_file): if result and os.path.isfile(self.temp_file):
os.remove(self.temp_file) os.remove(self.temp_file)
@ -281,11 +285,6 @@ class TorrentManager(component.Component):
'Paused', 'Paused',
'Queued', 'Queued',
): ):
# If the global setting is set, but the per-torrent isn't...
# Just skip to the next torrent.
# This is so that a user can turn-off the stop at ratio option on a per-torrent basis
if not torrent.options['stop_at_ratio']:
continue
if ( if (
torrent.get_ratio() >= torrent.options['stop_ratio'] torrent.get_ratio() >= torrent.options['stop_ratio']
and torrent.is_finished and torrent.is_finished
@ -293,7 +292,7 @@ class TorrentManager(component.Component):
if torrent.options['remove_at_ratio']: if torrent.options['remove_at_ratio']:
self.remove(torrent_id) self.remove(torrent_id)
break break
if not torrent.handle.status().paused: if not torrent.status.paused:
torrent.pause() torrent.pause()
def __getitem__(self, torrent_id): def __getitem__(self, torrent_id):
@ -346,26 +345,28 @@ class TorrentManager(component.Component):
else: else:
return torrent_info return torrent_info
def prefetch_metadata(self, magnet, timeout): @maybe_coroutine
async def prefetch_metadata(self, magnet: str, timeout: int) -> Tuple[str, bytes]:
"""Download the metadata for a magnet URI. """Download the metadata for a magnet URI.
Args: Args:
magnet (str): A magnet URI to download the metadata for. magnet: A magnet URI to download the metadata for.
timeout (int): Number of seconds to wait before canceling. timeout: Number of seconds to wait before canceling.
Returns: Returns:
Deferred: A tuple of (torrent_id (str), metadata (dict)) A tuple of (torrent_id, metadata)
""" """
torrent_id = get_magnet_info(magnet)['info_hash'] torrent_id = get_magnet_info(magnet)['info_hash']
if torrent_id in self.prefetching_metadata: if torrent_id in self.prefetching_metadata:
return self.prefetching_metadata[torrent_id].defer d = Deferred()
self.prefetching_metadata[torrent_id].result_queue.append(d)
return await d
add_torrent_params = {} add_torrent_params = lt.parse_magnet_uri(magnet)
add_torrent_params['save_path'] = gettempdir() add_torrent_params.save_path = gettempdir()
add_torrent_params['url'] = magnet.strip().encode('utf8') add_torrent_params.flags = (
add_torrent_params['flags'] = (
( (
LT_DEFAULT_ADD_TORRENT_FLAGS LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.add_torrent_params_flags_t.flag_duplicate_is_error | lt.add_torrent_params_flags_t.flag_duplicate_is_error
@ -379,33 +380,29 @@ class TorrentManager(component.Component):
d = Deferred() d = Deferred()
# Cancel the defer if timeout reached. # Cancel the defer if timeout reached.
defer_timeout = self.callLater(timeout, d.cancel) d.addTimeout(timeout, self.clock)
d.addBoth(self.on_prefetch_metadata, torrent_id, defer_timeout) self.prefetching_metadata[torrent_id] = PrefetchQueueItem(d, [])
Prefetch = namedtuple('Prefetch', 'defer handle')
self.prefetching_metadata[torrent_id] = Prefetch(defer=d, handle=torrent_handle)
return d
def on_prefetch_metadata(self, torrent_info, torrent_id, defer_timeout):
# Cancel reactor.callLater.
try: try:
defer_timeout.cancel() torrent_info = await d
except error.AlreadyCalled: except (defer.TimeoutError, defer.CancelledError):
pass log.debug(f'Prefetching metadata for {torrent_id} timed out or cancelled.')
metadata = b''
else:
log.debug('prefetch metadata received')
if VersionSplit(LT_VERSION) < VersionSplit('2.0.0.0'):
metadata = torrent_info.metadata()
else:
metadata = torrent_info.info_section()
log.debug('remove prefetch magnet from session') log.debug('remove prefetch magnet from session')
try: result_queue = self.prefetching_metadata.pop(torrent_id).result_queue
torrent_handle = self.prefetching_metadata.pop(torrent_id).handle self.session.remove_torrent(torrent_handle, 1)
except KeyError: result = torrent_id, b64encode(metadata)
pass
else:
self.session.remove_torrent(torrent_handle, 1)
metadata = None for d in result_queue:
if isinstance(torrent_info, lt.torrent_info): d.callback(result)
log.debug('prefetch metadata received') return result
metadata = lt.bdecode(torrent_info.metadata())
return torrent_id, metadata
def _build_torrent_options(self, options): def _build_torrent_options(self, options):
"""Load default options and update if needed.""" """Load default options and update if needed."""
@ -438,14 +435,10 @@ class TorrentManager(component.Component):
elif magnet: elif magnet:
magnet_info = get_magnet_info(magnet) magnet_info = get_magnet_info(magnet)
if magnet_info: if magnet_info:
add_torrent_params['url'] = magnet.strip().encode('utf8')
add_torrent_params['name'] = magnet_info['name'] add_torrent_params['name'] = magnet_info['name']
add_torrent_params['trackers'] = list(magnet_info['trackers'])
torrent_id = magnet_info['info_hash'] torrent_id = magnet_info['info_hash']
# Workaround lt 1.2 bug for magnet resume data with no metadata add_torrent_params['info_hash'] = bytes(bytearray.fromhex(torrent_id))
if resume_data and VersionSplit(LT_VERSION) >= VersionSplit('1.2.10.0'):
add_torrent_params['info_hash'] = bytes(
bytearray.fromhex(torrent_id)
)
else: else:
raise AddTorrentError( raise AddTorrentError(
'Unable to add magnet, invalid magnet info: %s' % magnet 'Unable to add magnet, invalid magnet info: %s' % magnet
@ -460,7 +453,7 @@ class TorrentManager(component.Component):
raise AddTorrentError('Torrent already being added (%s).' % torrent_id) raise AddTorrentError('Torrent already being added (%s).' % torrent_id)
elif torrent_id in self.prefetching_metadata: elif torrent_id in self.prefetching_metadata:
# Cancel and remove metadata fetching torrent. # Cancel and remove metadata fetching torrent.
self.prefetching_metadata[torrent_id].defer.cancel() self.prefetching_metadata[torrent_id].alert_deferred.cancel()
# Check for renamed files and if so, rename them in the torrent_info before adding. # Check for renamed files and if so, rename them in the torrent_info before adding.
if options['mapped_files'] and torrent_info: if options['mapped_files'] and torrent_info:
@ -821,12 +814,9 @@ class TorrentManager(component.Component):
try: try:
with open(filepath, 'rb') as _file: with open(filepath, 'rb') as _file:
if PY2: state = pickle.load(_file, encoding='utf8')
state = pickle.load(_file) except (OSError, EOFError, pickle.UnpicklingError) as ex:
else: message = f'Unable to load {filepath}: {ex}'
state = pickle.load(_file, encoding='utf8')
except (IOError, EOFError, pickle.UnpicklingError) as ex:
message = 'Unable to load {}: {}'.format(filepath, ex)
log.error(message) log.error(message)
if not filepath.endswith('.bak'): if not filepath.endswith('.bak'):
self.archive_state(message) self.archive_state(message)
@ -1082,7 +1072,7 @@ class TorrentManager(component.Component):
try: try:
with open(_filepath, 'rb') as _file: with open(_filepath, 'rb') as _file:
resume_data = lt.bdecode(_file.read()) resume_data = lt.bdecode(_file.read())
except (IOError, EOFError, RuntimeError) as ex: except (OSError, EOFError, RuntimeError) as ex:
if self.torrents: if self.torrents:
log.warning('Unable to load %s: %s', _filepath, ex) log.warning('Unable to load %s: %s', _filepath, ex)
resume_data = None resume_data = None
@ -1366,10 +1356,8 @@ class TorrentManager(component.Component):
torrent.set_tracker_status('Announce OK') torrent.set_tracker_status('Announce OK')
# Check for peer information from the tracker, if none then send a scrape request. # Check for peer information from the tracker, if none then send a scrape request.
if ( torrent.get_lt_status()
alert.handle.status().num_complete == -1 if torrent.status.num_complete == -1 or torrent.status.num_incomplete == -1:
or alert.handle.status().num_incomplete == -1
):
torrent.scrape_tracker() torrent.scrape_tracker()
def on_alert_tracker_announce(self, alert): def on_alert_tracker_announce(self, alert):
@ -1404,22 +1392,18 @@ class TorrentManager(component.Component):
log.debug( log.debug(
'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message 'Tracker Error Alert: %s [%s]', decode_bytes(alert.message()), error_message
) )
if VersionSplit(LT_VERSION) >= VersionSplit('1.2.0.0'): # libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates
# libtorrent 1.2 added endpoint struct to each tracker. to prevent false updates # we will need to verify that at least one endpoint to the errored tracker is working
# we will need to verify that at least one endpoint to the errored tracker is working for tracker in torrent.handle.trackers():
for tracker in torrent.handle.trackers(): if tracker['url'] == alert.url:
if tracker['url'] == alert.url: if any(
if any( endpoint['last_error']['value'] == 0
endpoint['last_error']['value'] == 0 for endpoint in tracker['endpoints']
for endpoint in tracker['endpoints'] ):
): torrent.set_tracker_status('Announce OK')
torrent.set_tracker_status('Announce OK') else:
else: torrent.set_tracker_status('Error: ' + error_message)
torrent.set_tracker_status('Error: ' + error_message) break
break
else:
# preserve old functionality for libtorrent < 1.2
torrent.set_tracker_status('Error: ' + error_message)
def on_alert_storage_moved(self, alert): def on_alert_storage_moved(self, alert):
"""Alert handler for libtorrent storage_moved_alert""" """Alert handler for libtorrent storage_moved_alert"""
@ -1493,7 +1477,9 @@ class TorrentManager(component.Component):
return return
if torrent_id in self.torrents: if torrent_id in self.torrents:
# libtorrent add_torrent expects bencoded resume_data. # libtorrent add_torrent expects bencoded resume_data.
self.resume_data[torrent_id] = lt.bencode(alert.resume_data) self.resume_data[torrent_id] = lt.bencode(
lt.write_resume_data(alert.params)
)
if torrent_id in self.waiting_on_resume_data: if torrent_id in self.waiting_on_resume_data:
self.waiting_on_resume_data[torrent_id].callback(None) self.waiting_on_resume_data[torrent_id].callback(None)
@ -1575,7 +1561,7 @@ class TorrentManager(component.Component):
# Try callback to prefetch_metadata method. # Try callback to prefetch_metadata method.
try: try:
d = self.prefetching_metadata[torrent_id].defer d = self.prefetching_metadata[torrent_id].alert_deferred
except KeyError: except KeyError:
pass pass
else: else:
@ -1621,7 +1607,7 @@ class TorrentManager(component.Component):
except RuntimeError: except RuntimeError:
continue continue
if torrent_id in self.torrents: if torrent_id in self.torrents:
self.torrents[torrent_id].update_status(t_status) self.torrents[torrent_id].status = t_status
self.handle_torrents_status_callback(self.torrents_status_requests.pop()) self.handle_torrents_status_callback(self.torrents_status_requests.pop())

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,10 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import division, print_function, unicode_literals import os
import stat
from OpenSSL import crypto
from OpenSSL.crypto import FILETYPE_PEM from OpenSSL.crypto import FILETYPE_PEM
from twisted.internet.ssl import ( from twisted.internet.ssl import (
AcceptableCiphers, AcceptableCiphers,
@ -18,6 +19,8 @@ from twisted.internet.ssl import (
TLSVersion, TLSVersion,
) )
import deluge.configmanager
# A TLS ciphers list. # A TLS ciphers list.
# Sources for more information on TLS ciphers: # Sources for more information on TLS ciphers:
# - https://wiki.mozilla.org/Security/Server_Side_TLS # - https://wiki.mozilla.org/Security/Server_Side_TLS
@ -77,3 +80,57 @@ def get_context_factory(cert_path, pkey_path):
ctx.set_options(SSL_OP_NO_RENEGOTIATION) ctx.set_options(SSL_OP_NO_RENEGOTIATION)
return cert_options return cert_options
def check_ssl_keys():
"""
Check for SSL cert/key and create them if necessary
"""
ssl_dir = deluge.configmanager.get_config_dir('ssl')
if not os.path.exists(ssl_dir):
# The ssl folder doesn't exist so we need to create it
os.makedirs(ssl_dir)
generate_ssl_keys()
else:
for f in ('daemon.pkey', 'daemon.cert'):
if not os.path.exists(os.path.join(ssl_dir, f)):
generate_ssl_keys()
break
def generate_ssl_keys():
"""
This method generates a new SSL key/cert.
"""
digest = 'sha256'
# Generate key pair
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
# Generate cert request
req = crypto.X509Req()
subj = req.get_subject()
setattr(subj, 'CN', 'Deluge Daemon')
req.set_pubkey(pkey)
req.sign(pkey, digest)
# Generate certificate
cert = crypto.X509()
cert.set_serial_number(0)
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(60 * 60 * 24 * 365 * 3) # Three Years
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, digest)
# Write out files
ssl_dir = deluge.configmanager.get_config_dir('ssl')
with open(os.path.join(ssl_dir, 'daemon.pkey'), 'wb') as _file:
_file.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
with open(os.path.join(ssl_dir, 'daemon.cert'), 'wb') as _file:
_file.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
# Make the files only readable by this user
for f in ('daemon.pkey', 'daemon.cert'):
os.chmod(os.path.join(ssl_dir, f), stat.S_IREAD | stat.S_IWRITE)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com> # Copyright (C) 2010 John Garland <johnnybg+deluge@gmail.com>
# #
@ -7,12 +6,13 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import inspect import inspect
import re import re
import warnings import warnings
from functools import wraps from functools import wraps
from typing import Any, Callable, Coroutine, TypeVar
from twisted.internet import defer
def proxy(proxy_func): def proxy(proxy_func):
@ -127,7 +127,7 @@ def _overrides(stack, method, explicit_base_classes=None):
% ( % (
method.__name__, method.__name__,
cls, cls,
'File: %s:%s' % (stack[1][1], stack[1][2]), f'File: {stack[1][1]}:{stack[1][2]}',
) )
) )
@ -137,7 +137,7 @@ def _overrides(stack, method, explicit_base_classes=None):
% ( % (
method.__name__, method.__name__,
check_classes, check_classes,
'File: %s:%s' % (stack[1][1], stack[1][2]), f'File: {stack[1][1]}:{stack[1][2]}',
) )
) )
return method return method
@ -154,7 +154,7 @@ def deprecated(func):
def depr_func(*args, **kwargs): def depr_func(*args, **kwargs):
warnings.simplefilter('always', DeprecationWarning) # Turn off filter warnings.simplefilter('always', DeprecationWarning) # Turn off filter
warnings.warn( warnings.warn(
'Call to deprecated function {}.'.format(func.__name__), f'Call to deprecated function {func.__name__}.',
category=DeprecationWarning, category=DeprecationWarning,
stacklevel=2, stacklevel=2,
) )
@ -162,3 +162,57 @@ def deprecated(func):
return func(*args, **kwargs) return func(*args, **kwargs)
return depr_func return depr_func
class CoroutineDeferred(defer.Deferred):
"""Wraps a coroutine in a Deferred.
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
from twisted.internet import reactor
super().__init__()
self.coro = coro
self.awaited = None
self.activate_deferred = reactor.callLater(0, self.activate)
def __await__(self):
if self.awaited in [None, True]:
self.awaited = True
return self.coro.__await__()
# Already in deferred mode
return super().__await__()
def activate(self):
"""If the result wasn't awaited before the next context switch, we turn it into a deferred."""
if self.awaited is None:
self.awaited = False
try:
d = defer.Deferred.fromCoroutine(self.coro)
except AttributeError:
# Fallback for Twisted <= 21.2 without fromCoroutine
d = defer.ensureDeferred(self.coro)
d.chainDeferred(self)
def addCallbacks(self, *args, **kwargs): # noqa: N802
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
self.activate()
return super().addCallbacks(*args, **kwargs)
_RetT = TypeVar('_RetT')
def maybe_coroutine(
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."""
@wraps(f)
def wrapper(*args, **kwargs):
# Uncomment for quick testing to make sure CoroutineDeferred magic isn't at fault
# return defer.ensureDeferred(f(*args, **kwargs))
return CoroutineDeferred(f(*args, **kwargs))
return wrapper

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -9,18 +8,15 @@
# #
from __future__ import unicode_literals
class DelugeError(Exception): class DelugeError(Exception):
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
inst = super(DelugeError, cls).__new__(cls, *args, **kwargs) inst = super().__new__(cls, *args, **kwargs)
inst._args = args inst._args = args
inst._kwargs = kwargs inst._kwargs = kwargs
return inst return inst
def __init__(self, message=None): def __init__(self, message=None):
super(DelugeError, self).__init__(message) super().__init__(message)
self.message = message self.message = message
def __str__(self): def __str__(self):
@ -45,12 +41,12 @@ class InvalidPathError(DelugeError):
class WrappedException(DelugeError): class WrappedException(DelugeError):
def __init__(self, message, exception_type, traceback): def __init__(self, message, exception_type, traceback):
super(WrappedException, self).__init__(message) super().__init__(message)
self.type = exception_type self.type = exception_type
self.traceback = traceback self.traceback = traceback
def __str__(self): def __str__(self):
return '%s\n%s' % (self.message, self.traceback) return f'{self.message}\n{self.traceback}'
class _ClientSideRecreateError(DelugeError): class _ClientSideRecreateError(DelugeError):
@ -64,7 +60,7 @@ class IncompatibleClient(_ClientSideRecreateError):
'Your deluge client is not compatible with the daemon. ' 'Your deluge client is not compatible with the daemon. '
'Please upgrade your client to %(daemon_version)s' 'Please upgrade your client to %(daemon_version)s'
) % {'daemon_version': self.daemon_version} ) % {'daemon_version': self.daemon_version}
super(IncompatibleClient, self).__init__(message=msg) super().__init__(message=msg)
class NotAuthorizedError(_ClientSideRecreateError): class NotAuthorizedError(_ClientSideRecreateError):
@ -73,14 +69,14 @@ class NotAuthorizedError(_ClientSideRecreateError):
'current_level': current_level, 'current_level': current_level,
'required_level': required_level, 'required_level': required_level,
} }
super(NotAuthorizedError, self).__init__(message=msg) super().__init__(message=msg)
self.current_level = current_level self.current_level = current_level
self.required_level = required_level self.required_level = required_level
class _UsernameBasedPasstroughError(_ClientSideRecreateError): class _UsernameBasedPasstroughError(_ClientSideRecreateError):
def __init__(self, message, username): def __init__(self, message, username):
super(_UsernameBasedPasstroughError, self).__init__(message) super().__init__(message)
self.username = username self.username = username

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -14,10 +13,6 @@ This module describes the types of events that can be generated by the daemon
and subsequently emitted to the clients. and subsequently emitted to the clients.
""" """
from __future__ import unicode_literals
import six
known_events = {} known_events = {}
@ -27,12 +22,12 @@ class DelugeEventMetaClass(type):
""" """
def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument def __init__(cls, name, bases, dct): # pylint: disable=bad-mcs-method-argument
super(DelugeEventMetaClass, cls).__init__(name, bases, dct) super().__init__(name, bases, dct)
if name != 'DelugeEvent': if name != 'DelugeEvent':
known_events[name] = cls known_events[name] = cls
class DelugeEvent(six.with_metaclass(DelugeEventMetaClass, object)): class DelugeEvent(metaclass=DelugeEventMetaClass):
""" """
The base class for all events. The base class for all events.

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import cgi import cgi
import logging import logging
import os.path import os.path
@ -19,7 +16,7 @@ from twisted.internet.defer import Deferred
from twisted.python.failure import Failure from twisted.python.failure import Failure
from twisted.web import client, http from twisted.web import client, http
from twisted.web._newclient import HTTPClientParser from twisted.web._newclient import HTTPClientParser
from twisted.web.error import PageRedirect from twisted.web.error import Error, PageRedirect
from twisted.web.http_headers import Headers from twisted.web.http_headers import Headers
from twisted.web.iweb import IAgent from twisted.web.iweb import IAgent
from zope.interface import implementer from zope.interface import implementer
@ -40,11 +37,11 @@ class CompressionDecoderProtocol(client._GzipProtocol):
"""A compression decoder protocol for CompressionDecoder.""" """A compression decoder protocol for CompressionDecoder."""
def __init__(self, protocol, response): def __init__(self, protocol, response):
super(CompressionDecoderProtocol, self).__init__(protocol, response) super().__init__(protocol, response)
self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS) self._zlibDecompress = zlib.decompressobj(32 + zlib.MAX_WBITS)
class BodyHandler(HTTPClientParser, object): class BodyHandler(HTTPClientParser):
"""An HTTP parser that saves the response to a file.""" """An HTTP parser that saves the response to a file."""
def __init__(self, request, finished, length, agent, encoding=None): def __init__(self, request, finished, length, agent, encoding=None):
@ -56,7 +53,7 @@ class BodyHandler(HTTPClientParser, object):
length (int): The length of the response. length (int): The length of the response.
agent (t.w.i.IAgent): The agent from which the request was sent. agent (t.w.i.IAgent): The agent from which the request was sent.
""" """
super(BodyHandler, self).__init__(request, finished) super().__init__(request, finished)
self.agent = agent self.agent = agent
self.finished = finished self.finished = finished
self.total_length = length self.total_length = length
@ -76,12 +73,12 @@ class BodyHandler(HTTPClientParser, object):
with open(self.agent.filename, 'wb') as _file: with open(self.agent.filename, 'wb') as _file:
_file.write(self.data) _file.write(self.data)
self.finished.callback(self.agent.filename) self.finished.callback(self.agent.filename)
self.state = u'DONE' self.state = 'DONE'
HTTPClientParser.connectionLost(self, reason) HTTPClientParser.connectionLost(self, reason)
@implementer(IAgent) @implementer(IAgent)
class HTTPDownloaderAgent(object): class HTTPDownloaderAgent:
"""A File Downloader Agent.""" """A File Downloader Agent."""
def __init__( def __init__(
@ -125,6 +122,9 @@ class HTTPDownloaderAgent(object):
location = response.headers.getRawHeaders(b'location')[0] location = response.headers.getRawHeaders(b'location')[0]
error = PageRedirect(response.code, location=location) error = PageRedirect(response.code, location=location)
finished.errback(Failure(error)) finished.errback(Failure(error))
elif response.code >= 400:
error = Error(response.code)
finished.errback(Failure(error))
else: else:
headers = response.headers headers = response.headers
body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0]) body_length = int(headers.getRawHeaders(b'content-length', default=[0])[0])
@ -146,7 +146,7 @@ class HTTPDownloaderAgent(object):
fileext = os.path.splitext(new_file_name)[1] fileext = os.path.splitext(new_file_name)[1]
while os.path.isfile(new_file_name): while os.path.isfile(new_file_name):
# Increment filename if already exists # Increment filename if already exists
new_file_name = '%s-%s%s' % (fileroot, count, fileext) new_file_name = f'{fileroot}-{count}{fileext}'
count += 1 count += 1
self.filename = new_file_name self.filename = new_file_name

View File

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*-
# #
# This file is public domain. # This file is public domain.
# #
from __future__ import unicode_literals
# Language code for this installation. All choices can be found here: # Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'en-us'

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,7 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals import builtins
import ctypes import ctypes
import gettext import gettext
import locale import locale
@ -17,8 +15,6 @@ import os
import sys import sys
from glob import glob from glob import glob
from six.moves import builtins
import deluge.common import deluge.common
from .languages import LANGUAGES from .languages import LANGUAGES
@ -80,7 +76,7 @@ def set_language(lang):
translation = gettext.translation( translation = gettext.translation(
'deluge', localedir=get_translations_path(), languages=[lang] 'deluge', localedir=get_translations_path(), languages=[lang]
) )
except IOError: except OSError:
log.warning('Unable to find translation (.mo) to set language: %s', lang) log.warning('Unable to find translation (.mo) to set language: %s', lang)
else: else:
translation.install() translation.install()
@ -113,19 +109,17 @@ def setup_translation():
gettext.bindtextdomain(I18N_DOMAIN, translations_path) gettext.bindtextdomain(I18N_DOMAIN, translations_path)
gettext.textdomain(I18N_DOMAIN) gettext.textdomain(I18N_DOMAIN)
# Workaround for Python 2 unicode gettext (keyword removed in Py3). gettext.install(I18N_DOMAIN, translations_path, names=['ngettext'])
kwargs = {} if not deluge.common.PY2 else {'unicode': True}
gettext.install(I18N_DOMAIN, translations_path, names=['ngettext'], **kwargs)
builtins.__dict__['_n'] = builtins.__dict__['ngettext'] builtins.__dict__['_n'] = builtins.__dict__['ngettext']
def load_libintl(libintls): def load_libintl(libintls):
errors = [] errors = []
libintl = None
for library in libintls: for library in libintls:
try: try:
libintl = ctypes.cdll.LoadLibrary(library) libintl = ctypes.cdll.LoadLibrary(library)
except OSError as ex: except OSError as ex:
errors.append(ex) errors.append(str(ex))
else: else:
break break

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2010 Pedro Algarvio <pedro@algarvio.me>
@ -9,8 +8,6 @@
# #
"""Logging functions""" """Logging functions"""
from __future__ import unicode_literals
import inspect import inspect
import logging import logging
import logging.handlers import logging.handlers
@ -39,7 +36,7 @@ MAX_LOGGER_NAME_LENGTH = 10
class Logging(LoggingLoggerClass): class Logging(LoggingLoggerClass):
def __init__(self, logger_name): def __init__(self, logger_name):
super(Logging, self).__init__(logger_name) super().__init__(logger_name)
# This makes module name padding increase to the biggest module name # This makes module name padding increase to the biggest module name
# so that logs keep readability. # so that logs keep readability.
@ -54,39 +51,31 @@ class Logging(LoggingLoggerClass):
) )
) )
@defer.inlineCallbacks
def garbage(self, msg, *args, **kwargs): def garbage(self, msg, *args, **kwargs):
yield LoggingLoggerClass.log(self, 1, msg, *args, **kwargs) LoggingLoggerClass.log(self, 1, msg, *args, **kwargs)
@defer.inlineCallbacks
def trace(self, msg, *args, **kwargs): def trace(self, msg, *args, **kwargs):
yield LoggingLoggerClass.log(self, 5, msg, *args, **kwargs) LoggingLoggerClass.log(self, 5, msg, *args, **kwargs)
@defer.inlineCallbacks
def debug(self, msg, *args, **kwargs): def debug(self, msg, *args, **kwargs):
yield LoggingLoggerClass.debug(self, msg, *args, **kwargs) LoggingLoggerClass.debug(self, msg, *args, **kwargs)
@defer.inlineCallbacks
def info(self, msg, *args, **kwargs): def info(self, msg, *args, **kwargs):
yield LoggingLoggerClass.info(self, msg, *args, **kwargs) LoggingLoggerClass.info(self, msg, *args, **kwargs)
@defer.inlineCallbacks
def warning(self, msg, *args, **kwargs): def warning(self, msg, *args, **kwargs):
yield LoggingLoggerClass.warning(self, msg, *args, **kwargs) LoggingLoggerClass.warning(self, msg, *args, **kwargs)
warn = warning warn = warning
@defer.inlineCallbacks
def error(self, msg, *args, **kwargs): def error(self, msg, *args, **kwargs):
yield LoggingLoggerClass.error(self, msg, *args, **kwargs) LoggingLoggerClass.error(self, msg, *args, **kwargs)
@defer.inlineCallbacks
def critical(self, msg, *args, **kwargs): def critical(self, msg, *args, **kwargs):
yield LoggingLoggerClass.critical(self, msg, *args, **kwargs) LoggingLoggerClass.critical(self, msg, *args, **kwargs)
@defer.inlineCallbacks
def exception(self, msg, *args, **kwargs): def exception(self, msg, *args, **kwargs):
yield LoggingLoggerClass.exception(self, msg, *args, **kwargs) LoggingLoggerClass.exception(self, msg, *args, **kwargs)
def findCaller(self, *args, **kwargs): # NOQA: N802 def findCaller(self, *args, **kwargs): # NOQA: N802
f = logging.currentframe().f_back f = logging.currentframe().f_back
@ -102,10 +91,7 @@ class Logging(LoggingLoggerClass):
continue continue
rv = (co.co_filename, f.f_lineno, co.co_name, None) rv = (co.co_filename, f.f_lineno, co.co_name, None)
break break
if common.PY2: return rv
return rv[:-1]
else:
return rv
levels = { levels = {
@ -161,7 +147,12 @@ def setup_logger(
handler_cls = getattr( handler_cls = getattr(
logging.handlers, 'WatchedFileHandler', logging.FileHandler logging.handlers, 'WatchedFileHandler', logging.FileHandler
) )
handler = handler_cls(filename, mode=filemode, encoding='utf-8') try:
handler = handler_cls(filename, mode=filemode, encoding='utf-8')
except FileNotFoundError:
handler = logging.StreamHandler(stream=output_stream)
log = logging.getLogger(__name__)
log.error(f'Unable to write to log file `{filename}`')
else: else:
handler = logging.StreamHandler(stream=output_stream) handler = logging.StreamHandler(stream=output_stream)
@ -243,7 +234,7 @@ def tweak_logging_levels():
log.warning( log.warning(
'logging.conf found! tweaking logging levels from %s', logging_config_file 'logging.conf found! tweaking logging levels from %s', logging_config_file
) )
with open(logging_config_file, 'r') as _file: with open(logging_config_file) as _file:
for line in _file: for line in _file:
if line.strip().startswith('#'): if line.strip().startswith('#'):
continue continue
@ -314,7 +305,7 @@ Triggering code:
""" """
class _BackwardsCompatibleLOG(object): class _BackwardsCompatibleLOG:
def __getattribute__(self, name): def __getattribute__(self, name):
import warnings import warnings

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import division, unicode_literals
import os import os
from hashlib import sha1 as sha from hashlib import sha1 as sha
@ -32,7 +29,7 @@ class InvalidPieceSize(Exception):
pass pass
class TorrentMetadata(object): class TorrentMetadata:
"""This class is used to create .torrent files. """This class is used to create .torrent files.
Examples: Examples:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Original file from BitTorrent-5.3-GPL.tar.gz # Original file from BitTorrent-5.3-GPL.tar.gz
# Copyright (C) Bram Cohen # Copyright (C) Bram Cohen
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import division, unicode_literals
import logging import logging
import os.path import os.path
import time import time
@ -44,7 +41,7 @@ def dummy(*v):
pass pass
class RemoteFileProgress(object): class RemoteFileProgress:
def __init__(self, session_id): def __init__(self, session_id):
self.session_id = session_id self.session_id = session_id

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2013 Bro <bro.development@gmail.com> # Copyright (C) 2013 Bro <bro.development@gmail.com>
# #
@ -8,12 +7,8 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os import os
from deluge.common import PY2
def is_hidden(filepath): def is_hidden(filepath):
def has_hidden_attribute(filepath): def has_hidden_attribute(filepath):
@ -45,7 +40,7 @@ def get_completion_paths(args):
:param args: options :param args: options
:type args: dict :type args: dict
:returns: the args argument containing the available completions for the completion_text :returns: the args argument containing the available completions for the completion_text
:rtype: list :rtype: dict
""" """
args['paths'] = [] args['paths'] = []
@ -54,10 +49,7 @@ def get_completion_paths(args):
def get_subdirs(dirname): def get_subdirs(dirname):
try: try:
if PY2: return next(os.walk(dirname))[1]
return os.walk(dirname).__next__[1]
else:
return next(os.walk(dirname))[1]
except StopIteration: except StopIteration:
# Invalid dirname # Invalid dirname
return [] return []

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
# #
@ -9,8 +8,7 @@
"""PluginManagerBase""" """PluginManagerBase"""
from __future__ import unicode_literals import email
import logging import logging
import os.path import os.path
@ -37,7 +35,7 @@ METADATA_KEYS = [
] ]
DEPRECATION_WARNING = """ DEPRECATION_WARNING = """
The plugin %s is not using the "deluge.plugins" namespace. The plugin %s is not using the "deluge_" namespace.
In order to avoid package name clashes between regular python packages and In order to avoid package name clashes between regular python packages and
deluge plugins, the way deluge plugins should be created has changed. deluge plugins, the way deluge plugins should be created has changed.
If you're seeing this message and you're not the developer of the plugin which If you're seeing this message and you're not the developer of the plugin which
@ -47,7 +45,7 @@ git repository to have an idea of what needs to be changed.
""" """
class PluginManagerBase(object): class PluginManagerBase:
"""PluginManagerBase is a base class for PluginManagers to inherit""" """PluginManagerBase is a base class for PluginManagers to inherit"""
def __init__(self, config_file, entry_name): def __init__(self, config_file, entry_name):
@ -164,7 +162,7 @@ class PluginManagerBase(object):
log.exception(ex) log.exception(ex)
return_d = defer.fail(False) return_d = defer.fail(False)
if not instance.__module__.startswith('deluge.plugins.'): if not instance.__module__.startswith('deluge_'):
import warnings import warnings
warnings.warn_explicit( warnings.warn_explicit(
@ -257,28 +255,25 @@ class PluginManagerBase(object):
def get_plugin_info(self, name): def get_plugin_info(self, name):
"""Returns a dictionary of plugin info from the metadata""" """Returns a dictionary of plugin info from the metadata"""
info = {}.fromkeys(METADATA_KEYS)
last_header = ''
cont_lines = []
# Missing plugin info
if not self.pkg_env[name]: if not self.pkg_env[name]:
log.warning('Failed to retrieve info for plugin: %s', name) log.warning('Failed to retrieve info for plugin: %s', name)
for k in info: info = {}.fromkeys(METADATA_KEYS, '')
info[k] = 'not available' info['Name'] = info['Version'] = 'not available'
return info return info
for line in self.pkg_env[name][0].get_metadata('PKG-INFO').splitlines():
if not line: pkg_info = self.pkg_env[name][0].get_metadata('PKG-INFO')
continue return self.parse_pkg_info(pkg_info)
if line[0] in ' \t' and (
len(line.split(':', 1)) == 1 or line.split(':', 1)[0] not in info @staticmethod
): def parse_pkg_info(pkg_info):
# This is a continuation metadata_msg = email.message_from_string(pkg_info)
cont_lines.append(line.strip()) metadata_ver = metadata_msg.get('Metadata-Version')
else:
if cont_lines: info = {key: metadata_msg.get(key, '') for key in METADATA_KEYS}
info[last_header] = '\n'.join(cont_lines).strip()
cont_lines = [] # Optional Description field in body (Metadata spec >=2.1)
if line.split(':', 1)[0] in info: if not info['Description'] and metadata_ver.startswith('2'):
last_header = line.split(':', 1)[0] info['Description'] = metadata_msg.get_payload().strip()
info[last_header] = line.split(':', 1)[1].strip()
return info return info

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> # Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -22,7 +19,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class Gtk3UIPlugin(PluginInitBase): class Gtk3UIPlugin(PluginInitBase):
@ -30,7 +27,7 @@ class Gtk3UIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(Gtk3UIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -38,4 +35,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from pkg_resources import resource_filename from pkg_resources import resource_filename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> # Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>
@ -13,8 +12,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import os import os
import shutil import shutil
@ -30,7 +27,7 @@ import deluge.configmanager
from deluge._libtorrent import lt from deluge._libtorrent import lt
from deluge.common import AUTH_LEVEL_ADMIN, is_magnet from deluge.common import AUTH_LEVEL_ADMIN, is_magnet
from deluge.core.rpcserver import export from deluge.core.rpcserver import export
from deluge.error import AddTorrentError from deluge.error import AddTorrentError, InvalidTorrentError
from deluge.event import DelugeEvent from deluge.event import DelugeEvent
from deluge.plugins.pluginbase import CorePluginBase from deluge.plugins.pluginbase import CorePluginBase
@ -152,7 +149,7 @@ class Core(CorePluginBase):
try: try:
with open(filename, file_mode) as _file: with open(filename, file_mode) as _file:
filedump = _file.read() filedump = _file.read()
except IOError as ex: except OSError as ex:
log.warning('Unable to open %s: %s', filename, ex) log.warning('Unable to open %s: %s', filename, ex)
raise ex raise ex
@ -161,7 +158,10 @@ class Core(CorePluginBase):
# Get the info to see if any exceptions are raised # Get the info to see if any exceptions are raised
if not magnet: if not magnet:
lt.torrent_info(lt.bdecode(filedump)) decoded_torrent = lt.bdecode(filedump)
if decoded_torrent is None:
raise InvalidTorrentError('Torrent file failed decoding.')
lt.torrent_info(decoded_torrent)
return filedump return filedump
@ -169,9 +169,9 @@ class Core(CorePluginBase):
log.debug('Attempting to open %s for splitting magnets.', filename) log.debug('Attempting to open %s for splitting magnets.', filename)
magnets = [] magnets = []
try: try:
with open(filename, 'r') as _file: with open(filename) as _file:
magnets = list(filter(len, _file.read().splitlines())) magnets = list(filter(len, _file.read().splitlines()))
except IOError as ex: except OSError as ex:
log.warning('Unable to open %s: %s', filename, ex) log.warning('Unable to open %s: %s', filename, ex)
if len(magnets) < 2: if len(magnets) < 2:
@ -196,7 +196,7 @@ class Core(CorePluginBase):
try: try:
with open(mname, 'w') as _mfile: with open(mname, 'w') as _mfile:
_mfile.write(magnet) _mfile.write(magnet)
except IOError as ex: except OSError as ex:
log.warning('Unable to open %s: %s', mname, ex) log.warning('Unable to open %s: %s', mname, ex)
return magnets return magnets
@ -271,7 +271,7 @@ class Core(CorePluginBase):
try: try:
filedump = self.load_torrent(filepath, magnet) filedump = self.load_torrent(filepath, magnet)
except (IOError, EOFError) as ex: except (OSError, EOFError, InvalidTorrentError) as ex:
# If torrent is invalid, keep track of it so can try again on the next pass. # If torrent is invalid, keep track of it so can try again on the next pass.
# This catches torrent files that may not be fully saved to disk at load time. # This catches torrent files that may not be fully saved to disk at load time.
log.debug('Torrent is invalid: %s', ex) log.debug('Torrent is invalid: %s', ex)

View File

@ -42,22 +42,21 @@ Deluge.ux.preferences.AutoAddPage = Ext.extend(Ext.Panel, {
dataIndex: 'enabled', dataIndex: 'enabled',
tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', { tpl: new Ext.XTemplate('{enabled:this.getCheckbox}', {
getCheckbox: function (checked, selected) { getCheckbox: function (checked, selected) {
Deluge.ux.AutoAdd.onClickFunctions[ Deluge.ux.AutoAdd.onClickFunctions[selected.id] =
selected.id function () {
] = function () { if (selected.enabled) {
if (selected.enabled) { deluge.client.autoadd.disable_watchdir(
deluge.client.autoadd.disable_watchdir( selected.id
selected.id );
); checked = false;
checked = false; } else {
} else { deluge.client.autoadd.enable_watchdir(
deluge.client.autoadd.enable_watchdir( selected.id
selected.id );
); checked = true;
checked = true; }
} autoAdd.updateWatchDirs();
autoAdd.updateWatchDirs(); };
};
return ( return (
'<input id="enabled-' + '<input id="enabled-' +
selected.id + selected.id +

View File

@ -90,9 +90,8 @@ Deluge.ux.AutoAdd.AutoAddWindowBase = Ext.extend(Ext.Window, {
options['enabled'] = Ext.getCmp('enabled').getValue(); options['enabled'] = Ext.getCmp('enabled').getValue();
options['path'] = Ext.getCmp('path').getValue(); options['path'] = Ext.getCmp('path').getValue();
options['download_location'] = Ext.getCmp( options['download_location'] =
'download_location' Ext.getCmp('download_location').getValue();
).getValue();
options['move_completed_path'] = Ext.getCmp( options['move_completed_path'] = Ext.getCmp(
'move_completed_path' 'move_completed_path'
).getValue(); ).getValue();

View File

@ -150,8 +150,6 @@
<property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory, <property name="tooltip_text" translatable="yes">If a .torrent file is added to this directory,
it will be added to the session.</property> it will be added to the session.</property>
<property name="invisible_char">●</property> <property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -284,8 +282,6 @@ and it will remain in the same directory.</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">•</property> <property name="invisible_char">•</property>
<property name="text" translatable="yes">.added</property> <property name="text" translatable="yes">.added</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -329,8 +325,6 @@ and deleted from the watch folder.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">•</property> <property name="invisible_char">•</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -445,8 +439,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">●</property> <property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -534,8 +526,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">●</property> <property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -799,8 +789,6 @@ also delete the .torrent file used to add it.</property>
<object class="GtkSpinButton" id="max_download_speed"> <object class="GtkSpinButton" id="max_download_speed">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment1</property> <property name="adjustment">adjustment1</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
<property name="digits">1</property> <property name="digits">1</property>
@ -815,8 +803,6 @@ also delete the .torrent file used to add it.</property>
<object class="GtkSpinButton" id="max_upload_speed"> <object class="GtkSpinButton" id="max_upload_speed">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment2</property> <property name="adjustment">adjustment2</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
<property name="digits">1</property> <property name="digits">1</property>
@ -833,8 +819,6 @@ also delete the .torrent file used to add it.</property>
<object class="GtkSpinButton" id="max_connections"> <object class="GtkSpinButton" id="max_connections">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment3</property> <property name="adjustment">adjustment3</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
</object> </object>
@ -850,8 +834,6 @@ also delete the .torrent file used to add it.</property>
<object class="GtkSpinButton" id="max_upload_slots"> <object class="GtkSpinButton" id="max_upload_slots">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment4</property> <property name="adjustment">adjustment4</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
</object> </object>
@ -1063,8 +1045,6 @@ also delete the .torrent file used to add it.</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">●</property> <property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment5</property> <property name="adjustment">adjustment5</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
<property name="digits">1</property> <property name="digits">1</property>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> # Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# #
@ -12,14 +11,12 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import os import os
import gi # isort:skip (Required before Gtk import). import gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version('Gtk', '3.0')
# isort:imports-thirdparty # isort:imports-thirdparty
from gi.repository import Gtk from gi.repository import Gtk
@ -41,7 +38,7 @@ class IncompatibleOption(Exception):
pass pass
class OptionsDialog(object): class OptionsDialog:
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio'] spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
spin_int_ids = ['max_upload_slots', 'max_connections'] spin_int_ids = ['max_upload_slots', 'max_connections']
chk_ids = [ chk_ids = [
@ -327,7 +324,7 @@ class OptionsDialog(object):
dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run() dialogs.ErrorDialog(_('Incompatible Option'), str(ex), self.dialog).run()
def on_error_show(self, result): def on_error_show(self, result):
d = dialogs.ErrorDialog(_('Error'), result.value.exception_msg, self.dialog) d = dialogs.ErrorDialog(_('Error'), result.value.message, self.dialog)
result.cleanFailure() result.cleanFailure()
d.run() d.run()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> # Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com> # Copyright (C) 2009 GazpachoKing <chase.sterling@gmail.com>
# Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2011 Pedro Algarvio <pedro@algarvio.me>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007-2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -17,7 +14,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -25,7 +22,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -33,4 +30,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,13 +11,10 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from functools import wraps from functools import wraps
from sys import exc_info from sys import exc_info
import six
from pkg_resources import resource_filename from pkg_resources import resource_filename
@ -47,7 +43,7 @@ def raises_errors_as(error):
return func(self, *args, **kwargs) return func(self, *args, **kwargs)
except Exception: except Exception:
(value, tb) = exc_info()[1:] (value, tb) = exc_info()[1:]
six.reraise(error, value, tb) raise error(value).with_traceback(tb) from None
return wrapper return wrapper
@ -74,7 +70,7 @@ class BadIP(Exception):
_message = None _message = None
def __init__(self, message): def __init__(self, message):
super(BadIP, self).__init__(message) super().__init__(message)
def __set_message(self, message): def __set_message(self, message):
self._message = message self._message = message
@ -86,7 +82,7 @@ class BadIP(Exception):
del __get_message, __set_message del __get_message, __set_message
class IP(object): class IP:
__slots__ = ('q1', 'q2', 'q3', 'q4', '_long') __slots__ = ('q1', 'q2', 'q3', 'q4', '_long')
def __init__(self, q1, q2, q3, q4): def __init__(self, q1, q2, q3, q4):
@ -109,7 +105,7 @@ class IP(object):
@classmethod @classmethod
def parse(cls, ip): def parse(cls, ip):
try: try:
q1, q2, q3, q4 = [int(q) for q in ip.split('.')] q1, q2, q3, q4 = (int(q) for q in ip.split('.'))
except ValueError: except ValueError:
raise BadIP(_('The IP address "%s" is badly formed' % ip)) raise BadIP(_('The IP address "%s" is badly formed' % ip))
if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0: if q1 < 0 or q2 < 0 or q3 < 0 or q4 < 0:
@ -169,7 +165,7 @@ class IP(object):
return self.long == other.long return self.long == other.long
def __repr__(self): def __repr__(self):
return '<%s long=%s address="%s">' % ( return '<{} long={} address="{}">'.format(
self.__class__.__name__, self.__class__.__name__,
self.long, self.long,
self.address, self.address,

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> # Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
@ -8,14 +7,13 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import division, unicode_literals
import logging import logging
import os import os
import shutil import shutil
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from email.utils import formatdate from email.utils import formatdate
from urllib.parse import urljoin
from twisted.internet import defer, threads from twisted.internet import defer, threads
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
@ -32,12 +30,6 @@ from .common import IP, BadIP
from .detect import UnknownFormatError, create_reader, detect_compression, detect_format from .detect import UnknownFormatError, create_reader, detect_compression, detect_format
from .readers import ReaderParseError from .readers import ReaderParseError
try:
from urllib.parse import urljoin
except ImportError:
# PY2 fallback
from urlparse import urljoin # pylint: disable=ungrouped-imports
# TODO: check return values for deferred callbacks # TODO: check return values for deferred callbacks
# TODO: review class attributes for redundancy # TODO: review class attributes for redundancy

View File

@ -55,7 +55,7 @@ Deluge.ux.preferences.BlocklistPage = Ext.extend(Ext.Panel, {
}); });
this.checkListDays = this.SettingsFset.add({ this.checkListDays = this.SettingsFset.add({
fieldLabel: _('Check for new list every:'), fieldLabel: _('Check for new list every (days):'),
labelSeparator: '', labelSeparator: '',
name: 'check_list_days', name: 'check_list_days',
value: 4, value: 4,

View File

@ -53,8 +53,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="invisible_char">●</property> <property name="invisible_char">●</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -124,8 +122,6 @@
<object class="GtkSpinButton" id="spin_check_days"> <object class="GtkSpinButton" id="spin_check_days">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment1</property> <property name="adjustment">adjustment1</property>
</object> </object>
<packing> <packing>
@ -139,7 +135,7 @@
<object class="GtkLabel" id="label4"> <object class="GtkLabel" id="label4">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="label" translatable="yes">Check for new list every:</property> <property name="label" translatable="yes">Check for new list every (days):</property>
<property name="xalign">0</property> <property name="xalign">0</property>
</object> </object>
<packing> <packing>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> # Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
# #
@ -8,8 +7,6 @@
# #
# pylint: disable=redefined-builtin # pylint: disable=redefined-builtin
from __future__ import unicode_literals
import bz2 import bz2
import gzip import gzip
import zipfile import zipfile

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> # Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from .decompressers import BZipped2, GZipped, Zipped from .decompressers import BZipped2, GZipped, Zipped
from .readers import EmuleReader, PeerGuardianReader, SafePeerReader from .readers import EmuleReader, PeerGuardianReader, SafePeerReader

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2008 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,14 +6,12 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from datetime import datetime from datetime import datetime
import gi # isort:skip (Required before Gtk import). import gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version('Gtk', '3.0')
# isort:imports-thirdparty # isort:imports-thirdparty
from gi.repository import Gtk from gi.repository import Gtk

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net) # Copyright (C) 2007 Steve 'Tarka' Smith (tarka@internode.on.net)
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import gzip import gzip
import logging import logging
import socket import socket
@ -23,14 +20,14 @@ class PGException(Exception):
# Incrementally reads PeerGuardian blocklists v1 and v2. # Incrementally reads PeerGuardian blocklists v1 and v2.
# See http://wiki.phoenixlabs.org/wiki/P2B_Format # See http://wiki.phoenixlabs.org/wiki/P2B_Format
class PGReader(object): class PGReader:
def __init__(self, filename): def __init__(self, filename):
log.debug('PGReader loading: %s', filename) log.debug('PGReader loading: %s', filename)
try: try:
with gzip.open(filename, 'rb') as _file: with gzip.open(filename, 'rb') as _file:
self.fd = _file self.fd = _file
except IOError: except OSError:
log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt') log.debug('Blocklist: PGReader: Incorrect file type or list is corrupt')
# 4 bytes, should be 0xffffffff # 4 bytes, should be 0xffffffff
@ -65,8 +62,5 @@ class PGReader(object):
return (start, end) return (start, end)
# Python 2 compatibility
next = __next__
def close(self): def close(self):
self.fd.close() self.fd.close()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com> # Copyright (C) 2009-2010 John Garland <johnnybg+deluge@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import re import re
@ -23,7 +20,7 @@ class ReaderParseError(Exception):
pass pass
class BaseReader(object): class BaseReader:
"""Base reader for blocklist files""" """Base reader for blocklist files"""
def __init__(self, _file): def __init__(self, _file):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> # Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -17,7 +14,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -25,7 +22,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -33,4 +30,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from pkg_resources import resource_filename from pkg_resources import resource_filename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import hashlib import hashlib
import logging import logging
import os import os

View File

@ -71,8 +71,6 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="can_default">True</property> <property name="can_default">True</property>
<property name="has_default">True</property> <property name="has_default">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> # Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
# #
@ -7,13 +6,11 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import gi # isort:skip (Required before Gtk import). import gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version('Gtk', '3.0')
# isort:imports-thirdparty # isort:imports-thirdparty
from gi.repository import Gtk from gi.repository import Gtk
@ -41,7 +38,7 @@ EVENT_MAP = {
EVENTS = ['complete', 'added', 'removed'] EVENTS = ['complete', 'added', 'removed']
class ExecutePreferences(object): class ExecutePreferences:
def __init__(self, plugin): def __init__(self, plugin):
self.plugin = plugin self.plugin = plugin

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> # Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com> # Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
# #

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from pkg_resources import resource_filename from pkg_resources import resource_filename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import errno import errno
import logging import logging
import os import os
@ -37,14 +34,11 @@ if windows_check():
'C:\\Program Files (x86)\\7-Zip\\7z.exe', 'C:\\Program Files (x86)\\7-Zip\\7z.exe',
] ]
try: import winreg
import winreg
except ImportError:
import _winreg as winreg # For Python 2.
try: try:
hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip') hkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, 'Software\\7-Zip')
except WindowsError: except OSError:
pass pass
else: else:
win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe') win_7z_path = os.path.join(winreg.QueryValueEx(hkey, 'Path')[0], '7z.exe')

View File

@ -62,8 +62,6 @@
<child> <child>
<object class="GtkEntry" id="entry_path"> <object class="GtkEntry" id="entry_path">
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,13 +10,11 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import gi # isort:skip (Required before Gtk import). import gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version('Gtk', '3.0')
# isort:imports-thirdparty # isort:imports-thirdparty
from gi.repository import Gtk from gi.repository import Gtk

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from pkg_resources import resource_filename from pkg_resources import resource_filename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -15,8 +14,6 @@
torrent-label core plugin. torrent-label core plugin.
adds a status field for tracker. adds a status field for tracker.
""" """
from __future__ import unicode_literals
import logging import logging
import re import re

View File

@ -148,8 +148,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
xtype: 'fieldset', xtype: 'fieldset',
border: false, border: false,
labelWidth: 1, labelWidth: 1,
style: style: 'margin-bottom: 0px; padding-bottom: 0px;',
'margin-bottom: 0px; padding-bottom: 0px;',
items: [ items: [
{ {
xtype: 'checkbox', xtype: 'checkbox',
@ -218,8 +217,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
xtype: 'fieldset', xtype: 'fieldset',
border: false, border: false,
labelWidth: 1, labelWidth: 1,
style: style: 'margin-bottom: 0px; padding-bottom: 0px;',
'margin-bottom: 0px; padding-bottom: 0px;',
items: [ items: [
{ {
xtype: 'checkbox', xtype: 'checkbox',
@ -260,8 +258,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
width: 60, width: 60,
decimalPrecision: 2, decimalPrecision: 2,
incrementValue: 0.1, incrementValue: 0.1,
style: style: 'position: relative; left: 100px',
'position: relative; left: 100px',
disabled: true, disabled: true,
}, },
{ {
@ -285,8 +282,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
xtype: 'fieldset', xtype: 'fieldset',
border: false, border: false,
labelWidth: 1, labelWidth: 1,
style: style: 'margin-bottom: 0px; padding-bottom: 0px;',
'margin-bottom: 0px; padding-bottom: 0px;',
items: [ items: [
{ {
xtype: 'checkbox', xtype: 'checkbox',
@ -339,8 +335,7 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
xtype: 'fieldset', xtype: 'fieldset',
border: false, border: false,
labelWidth: 1, labelWidth: 1,
style: style: 'margin-bottom: 0px; padding-bottom: 0px;',
'margin-bottom: 0px; padding-bottom: 0px;',
items: [ items: [
{ {
xtype: 'checkbox', xtype: 'checkbox',
@ -408,9 +403,8 @@ Deluge.ux.LabelOptionsWindow = Ext.extend(Ext.Window, {
onOkClick: function () { onOkClick: function () {
var values = this.form.getForm().getFieldValues(); var values = this.form.getForm().getFieldValues();
if (values['auto_add_trackers']) { if (values['auto_add_trackers']) {
values['auto_add_trackers'] = values['auto_add_trackers'].split( values['auto_add_trackers'] =
'\n' values['auto_add_trackers'].split('\n');
);
} }
deluge.client.label.set_options(this.label, values); deluge.client.label.set_options(this.label, values);
this.hide(); this.hide();

View File

@ -141,8 +141,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="activates_default">True</property> <property name="activates_default">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>

View File

@ -209,8 +209,6 @@
<object class="GtkSpinButton" id="max_upload_speed"> <object class="GtkSpinButton" id="max_upload_speed">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment1</property> <property name="adjustment">adjustment1</property>
</object> </object>
<packing> <packing>
@ -239,8 +237,6 @@
<object class="GtkSpinButton" id="max_download_speed"> <object class="GtkSpinButton" id="max_download_speed">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment2</property> <property name="adjustment">adjustment2</property>
</object> </object>
<packing> <packing>
@ -310,8 +306,6 @@
<object class="GtkSpinButton" id="max_upload_slots"> <object class="GtkSpinButton" id="max_upload_slots">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment3</property> <property name="adjustment">adjustment3</property>
<property name="numeric">True</property> <property name="numeric">True</property>
</object> </object>
@ -342,8 +336,6 @@
<object class="GtkSpinButton" id="max_connections"> <object class="GtkSpinButton" id="max_connections">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment4</property> <property name="adjustment">adjustment4</property>
<property name="numeric">True</property> <property name="numeric">True</property>
</object> </object>
@ -483,8 +475,6 @@
<object class="GtkSpinButton" id="stop_ratio"> <object class="GtkSpinButton" id="stop_ratio">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment5</property> <property name="adjustment">adjustment5</property>
<property name="digits">2</property> <property name="digits">2</property>
</object> </object>
@ -599,8 +589,6 @@
<child> <child>
<object class="GtkEntry" id="move_completed_path_entry"> <object class="GtkEntry" id="move_completed_path_entry">
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge import component # for systray from deluge import component # for systray

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -7,8 +6,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from gi.repository.Gtk import Builder from gi.repository.Gtk import Builder
@ -20,7 +17,7 @@ from ..common import get_resource
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
class LabelConfig(object): class LabelConfig:
""" """
there used to be some options here... there used to be some options here...
""" """

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2007 Andrew Resch <andrewresch@gmail.com>
@ -8,13 +7,11 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import gi # isort:skip (Required before Gtk import). import gi # isort:skip (Required before Gtk import).
gi.require_version('Gtk', '3.0') # NOQA: E402 gi.require_version('Gtk', '3.0')
# isort:imports-thirdparty # isort:imports-thirdparty
from gi.repository import Gtk from gi.repository import Gtk
@ -32,7 +29,7 @@ NO_LABEL = 'No Label'
# menu # menu
class LabelSidebarMenu(object): class LabelSidebarMenu:
def __init__(self): def __init__(self):
self.treeview = component.get('FilterTreeView') self.treeview = component.get('FilterTreeView')
@ -107,7 +104,7 @@ class LabelSidebarMenu(object):
# dialogs: # dialogs:
class AddDialog(object): class AddDialog:
def __init__(self): def __init__(self):
pass pass
@ -129,7 +126,7 @@ class AddDialog(object):
self.dialog.destroy() self.dialog.destroy()
class OptionsDialog(object): class OptionsDialog:
spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio'] spin_ids = ['max_download_speed', 'max_upload_speed', 'stop_ratio']
spin_int_ids = ['max_upload_slots', 'max_connections'] spin_int_ids = ['max_upload_slots', 'max_connections']
chk_ids = [ chk_ids = [
@ -174,7 +171,7 @@ class OptionsDialog(object):
self.builder.connect_signals(self) self.builder.connect_signals(self)
# Show the label name in the header label # Show the label name in the header label
self.builder.get_object('label_header').set_markup( self.builder.get_object('label_header').set_markup(
'<b>%s:</b> %s' % (_('Label Options'), self.label) '<b>{}:</b> {}'.format(_('Label Options'), self.label)
) )
for chk_id, group in self.sensitive_groups: for chk_id, group in self.sensitive_groups:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -8,8 +7,6 @@
# #
from __future__ import unicode_literals
import logging import logging
from gi.repository.Gtk import Menu, MenuItem from gi.repository.Gtk import Menu, MenuItem

View File

@ -1,5 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
# #
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
@ -10,8 +9,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import print_function, unicode_literals
from deluge.ui.client import sclient from deluge.ui.client import sclient
sclient.set_core_uri() sclient.set_core_uri()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
# #

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -22,7 +19,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -30,7 +27,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -38,4 +35,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import os.path import os.path
@ -30,7 +27,7 @@ def get_resource(filename):
return resource_filename(__package__, os.path.join('data', filename)) return resource_filename(__package__, os.path.join('data', filename))
class CustomNotifications(object): class CustomNotifications:
def __init__(self, plugin_name=None): def __init__(self, plugin_name=None):
self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}} self.custom_notifications = {'email': {}, 'popup': {}, 'blink': {}, 'sound': {}}

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import smtplib import smtplib
from email.utils import formatdate from email.utils import formatdate
@ -119,7 +116,6 @@ Date: %(date)s
message = '\r\n'.join((headers + message).splitlines()) message = '\r\n'.join((headers + message).splitlines())
try: try:
# Python 2.6
server = smtplib.SMTP( server = smtplib.SMTP(
self.config['smtp_host'], self.config['smtp_port'], timeout=60 self.config['smtp_host'], self.config['smtp_port'], timeout=60
) )
@ -152,7 +148,7 @@ Date: %(date)s
try: try:
try: try:
server.sendmail(self.config['smtp_from'], to_addrs, message) server.sendmail(self.config['smtp_from'], to_addrs, message.encode())
except smtplib.SMTPException as ex: except smtplib.SMTPException as ex:
err_msg = ( err_msg = (
_('There was an error sending the notification email: %s') % ex _('There was an error sending the notification email: %s') % ex

View File

@ -187,8 +187,6 @@
<object class="GtkEntry" id="smtp_host"> <object class="GtkEntry" id="smtp_host">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -217,8 +215,6 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="max_length">5</property> <property name="max_length">5</property>
<property name="width_chars">5</property> <property name="width_chars">5</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="adjustment">adjustment1</property> <property name="adjustment">adjustment1</property>
<property name="climb_rate">1</property> <property name="climb_rate">1</property>
<property name="numeric">True</property> <property name="numeric">True</property>
@ -246,8 +242,6 @@
<object class="GtkEntry" id="smtp_user"> <object class="GtkEntry" id="smtp_user">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -273,8 +267,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="visibility">False</property> <property name="visibility">False</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -427,8 +419,6 @@
<object class="GtkEntry" id="smtp_from"> <object class="GtkEntry" id="smtp_from">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from os.path import basename from os.path import basename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# vim: sw=4 ts=4 fenc=utf-8 et # vim: sw=4 ts=4 fenc=utf-8 et
# ============================================================================== # ==============================================================================
# Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me> # Copyright © 2009-2010 UfSoft.org - Pedro Algarvio <pedro@algarvio.me>
@ -6,8 +5,6 @@
# License: BSD - Please view the LICENSE file for additional information. # License: BSD - Please view the LICENSE file for additional information.
# ============================================================================== # ==============================================================================
from __future__ import unicode_literals
import logging import logging
from twisted.internet import task from twisted.internet import task
@ -70,14 +67,14 @@ class TestEmailNotifications(component.Component):
def custom_email_message_provider(self, *evt_args, **evt_kwargs): def custom_email_message_provider(self, *evt_args, **evt_kwargs):
log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs) log.debug('Running custom email message provider: %s %s', evt_args, evt_kwargs)
subject = '%s Email Subject: %s' % (self.events[0].__class__.__name__, self.n) subject = f'{self.events[0].__class__.__name__} Email Subject: {self.n}'
message = '%s Email Message: %s' % (self.events[0].__class__.__name__, self.n) message = f'{self.events[0].__class__.__name__} Email Message: {self.n}'
return subject, message return subject, message
def custom_popup_message_provider(self, *evt_args, **evt_kwargs): def custom_popup_message_provider(self, *evt_args, **evt_kwargs):
log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs) log.debug('Running custom popup message provider: %s %s', evt_args, evt_kwargs)
title = '%s Popup Title: %s' % (self.events[0].__class__.__name__, self.n) title = f'{self.events[0].__class__.__name__} Popup Title: {self.n}'
message = '%s Popup Message: %s' % (self.events[0].__class__.__name__, self.n) message = f'{self.events[0].__class__.__name__} Popup Message: {self.n}'
return title, message return title, message
def custom_blink_message_provider(self, *evt_args, **evt_kwargs): def custom_blink_message_provider(self, *evt_args, **evt_kwargs):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
from deluge.plugins.pluginbase import WebPluginBase from deluge.plugins.pluginbase import WebPluginBase

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me> # Copyright (C) 2009-2010 Pedro Algarvio <pedro@algarvio.me>
# #

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
from deluge.plugins.init import PluginInitBase from deluge.plugins.init import PluginInitBase
@ -21,7 +18,7 @@ class CorePlugin(PluginInitBase):
from .core import Core as _pluginCls from .core import Core as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(CorePlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class GtkUIPlugin(PluginInitBase): class GtkUIPlugin(PluginInitBase):
@ -29,7 +26,7 @@ class GtkUIPlugin(PluginInitBase):
from .gtkui import GtkUI as _pluginCls from .gtkui import GtkUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(GtkUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)
class WebUIPlugin(PluginInitBase): class WebUIPlugin(PluginInitBase):
@ -37,4 +34,4 @@ class WebUIPlugin(PluginInitBase):
from .webui import WebUI as _pluginCls from .webui import WebUI as _pluginCls
self._plugin_cls = _pluginCls self._plugin_cls = _pluginCls
super(WebUIPlugin, self).__init__(plugin_name) super().__init__(plugin_name)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Basic plugin template created by: # Basic plugin template created by:
# Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com> # Copyright (C) 2008 Martijn Voncken <mvoncken@gmail.com>
@ -12,8 +11,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import os.path import os.path
from pkg_resources import resource_filename from pkg_resources import resource_filename

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# #
# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> # Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com>
# #
@ -11,8 +10,6 @@
# See LICENSE for more details. # See LICENSE for more details.
# #
from __future__ import unicode_literals
import logging import logging
import time import time

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