Skip to content

Commit a8abea3

Browse files
committed
Add static typing
1 parent f1d2789 commit a8abea3

File tree

11 files changed

+188
-147
lines changed

11 files changed

+188
-147
lines changed

.github/workflows/ci.yaml

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,29 @@ jobs:
2929
python-version: ${{ matrix.python-version }}
3030

3131
- name: Install tools
32-
run: pip install bandit isort mypy pycodestyle pyflakes pyright pytest
32+
run: |
33+
pip install bandit isort mypy pycodestyle pyflakes pyright pytest
34+
./nova3/aux/fetch_aux.sh
3335
3436
- name: Run tests
3537
if: matrix.python-version == '3.10' # avoid hammering the sites
3638
continue-on-error: true
3739
run: |
38-
./nova3/aux/fetch_aux.sh
40+
cd nova3
3941
pytest \
4042
--showlocals \
41-
nova3/tests/*.py
43+
tests/*.py
4244
4345
- name: Check typings
4446
run: |
47+
cd nova3
4548
mypy \
46-
--follow-imports skip \
4749
--strict \
48-
nova3/tests/*.py
50+
engines/*.py \
51+
tests/*.py
4952
pyright \
50-
--skipunannotated \
51-
nova3/tests/*.py
53+
engines/*.py \
54+
tests/*.py
5255
5356
- name: Lint code
5457
run: |
@@ -72,6 +75,7 @@ jobs:
7275
isort \
7376
--check \
7477
--diff \
78+
--line-length 1000 \
7579
nova3/engines/*.py \
7680
nova3/tests/*.py
7781

nova3/engines/eztv.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1-
# VERSION: 1.19
1+
# VERSION: 1.20
22
# AUTHORS: nindogo
33
# CONTRIBUTORS: Diego de las Heras ([email protected])
44

5+
import http.client
56
import re
67
import sys
78
import urllib.error
89
import urllib.parse
910
import urllib.request
1011
from datetime import datetime, timedelta
1112
from html.parser import HTMLParser
13+
from typing import Any, Callable, Dict, List, Mapping, Match, Tuple, Union
1214

1315
from helpers import retrieve_url
1416
from novaprinter import prettyPrinter
1517

1618

17-
class eztv(object):
19+
class eztv:
1820
name = "EZTV"
1921
url = 'https://eztvx.to/'
2022
supported_categories = {'all': 'all', 'tv': 'tv'}
@@ -23,22 +25,26 @@ class MyHtmlParser(HTMLParser):
2325
A, TD, TR, TABLE = ('a', 'td', 'tr', 'table')
2426

2527
""" Sub-class for parsing results """
26-
def __init__(self, url):
28+
def __init__(self, url: str) -> None:
2729
HTMLParser.__init__(self)
2830
self.url = url
2931

3032
now = datetime.now()
31-
self.date_parsers = {
33+
self.date_parsers: Mapping[str, Callable[[Match[str]], datetime]] = {
3234
r"(\d+)h\s+(\d+)m": lambda m: now - timedelta(hours=int(m[1]), minutes=int(m[2])),
3335
r"(\d+)d\s+(\d+)h": lambda m: now - timedelta(days=int(m[1]), hours=int(m[2])),
3436
r"(\d+)\s+weeks?": lambda m: now - timedelta(weeks=int(m[1])),
3537
r"(\d+)\s+mo": lambda m: now - timedelta(days=int(m[1]) * 30),
3638
r"(\d+)\s+years?": lambda m: now - timedelta(days=int(m[1]) * 365),
3739
}
3840
self.in_table_row = False
39-
self.current_item = {}
41+
self.current_item: Dict[str, Any] = {}
42+
43+
def handle_starttag(self, tag: str, attrs: List[Tuple[str, Union[str, None]]]) -> None:
44+
def getStr(d: Mapping[str, Any], key: str) -> str:
45+
value = d.get(key, '')
46+
return value if value is not None else ''
4047

41-
def handle_starttag(self, tag, attrs):
4248
params = dict(attrs)
4349

4450
if (params.get('class') == 'forum_header_border'
@@ -57,10 +63,10 @@ def handle_starttag(self, tag, attrs):
5763

5864
if (tag == self.A
5965
and self.in_table_row and params.get('class') == 'epinfo'):
60-
self.current_item['desc_link'] = self.url + params.get('href')
61-
self.current_item['name'] = params.get('title').split(' (')[0]
66+
self.current_item['desc_link'] = self.url + getStr(params, 'href')
67+
self.current_item['name'] = getStr(params, 'title').split(' (')[0]
6268

63-
def handle_data(self, data):
69+
def handle_data(self, data: str) -> None:
6470
data = data.replace(',', '')
6571
if (self.in_table_row
6672
and (data.endswith(' KB') or data.endswith(' MB') or data.endswith(' GB'))):
@@ -76,12 +82,12 @@ def handle_data(self, data):
7682
self.current_item["pub_date"] = int(calc(m).timestamp())
7783
break
7884

79-
def handle_endtag(self, tag):
85+
def handle_endtag(self, tag: str) -> None:
8086
if self.in_table_row and tag == self.TR:
81-
prettyPrinter(self.current_item)
87+
prettyPrinter(self.current_item) # type: ignore[arg-type] # refactor later
8288
self.in_table_row = False
8389

84-
def do_query(self, what):
90+
def do_query(self, what: str) -> str:
8591
url = f"{self.url}/search/{what.replace('%20', '-')}"
8692
data = b"layout=def_wlinks"
8793
try:
@@ -92,13 +98,13 @@ def do_query(self, what):
9298
user_agent = 'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0'
9399
req = urllib.request.Request(url, data, {'User-Agent': user_agent})
94100
try:
95-
response = urllib.request.urlopen(req) # nosec B310
101+
response: http.client.HTTPResponse = urllib.request.urlopen(req) # nosec B310
96102
return response.read().decode('utf-8')
97103
except urllib.error.URLError as errno:
98104
print(f"Connection error: {errno.reason}", file=sys.stderr)
99105
return ""
100106

101-
def search(self, what, cat='all'):
107+
def search(self, what: str, cat: str = 'all') -> None:
102108
eztv_html = self.do_query(what)
103109

104110
eztv_parser = self.MyHtmlParser(self.url)

nova3/engines/jackett.py

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# VERSION: 4.4
1+
# VERSION: 4.5
22
# AUTHORS: Diego de las Heras ([email protected])
33
# CONTRIBUTORS: ukharley
44
# hannsen (github.com/hannsen)
@@ -12,6 +12,7 @@
1212
from http.cookiejar import CookieJar
1313
from multiprocessing.dummy import Pool
1414
from threading import Lock
15+
from typing import Any, Dict, List, Union
1516
from urllib.parse import unquote, urlencode
1617

1718
import helpers
@@ -53,7 +54,7 @@ def enable_proxy(self, enable: bool) -> None:
5354
# load configuration from file
5455
CONFIG_FILE = 'jackett.json'
5556
CONFIG_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), CONFIG_FILE)
56-
CONFIG_DATA = {
57+
CONFIG_DATA: Dict[str, Any] = {
5758
'api_key': 'YOUR_API_KEY_HERE', # jackett api
5859
'url': 'http://127.0.0.1:9117', # jackett url
5960
'tracker_first': False, # (False/True) add tracker name to beginning of search result
@@ -62,7 +63,7 @@ def enable_proxy(self, enable: bool) -> None:
6263
PRINTER_THREAD_LOCK = Lock()
6364

6465

65-
def load_configuration():
66+
def load_configuration() -> None:
6667
global CONFIG_DATA
6768
try:
6869
# try to load user data from file
@@ -85,7 +86,7 @@ def load_configuration():
8586
save_configuration()
8687

8788

88-
def save_configuration():
89+
def save_configuration() -> None:
8990
with open(CONFIG_PATH, 'w') as f:
9091
f.write(json.dumps(CONFIG_DATA, indent=4, sort_keys=True))
9192

@@ -94,7 +95,7 @@ def save_configuration():
9495
###############################################################################
9596

9697

97-
class jackett(object):
98+
class jackett:
9899
name = 'Jackett'
99100
url = CONFIG_DATA['url'] if CONFIG_DATA['url'][-1] != '/' else CONFIG_DATA['url'][:-1]
100101
api_key = CONFIG_DATA['api_key']
@@ -110,7 +111,7 @@ class jackett(object):
110111
'tv': ['5000'],
111112
}
112113

113-
def download_torrent(self, download_url):
114+
def download_torrent(self, download_url: str) -> None:
114115
# fix for some indexers with magnet link inside .torrent file
115116
if download_url.startswith('magnet:?'):
116117
print(download_url + " " + download_url)
@@ -122,7 +123,7 @@ def download_torrent(self, download_url):
122123
else:
123124
print(helpers.download_file(download_url))
124125

125-
def search(self, what, cat='all'):
126+
def search(self, what: str, cat: str = 'all') -> None:
126127
what = unquote(what)
127128
category = self.supported_categories[cat.lower()]
128129

@@ -147,52 +148,59 @@ def search(self, what, cat='all'):
147148
else:
148149
self.search_jackett_indexer(what, category, 'all')
149150

150-
def get_jackett_indexers(self, what):
151-
params = [
151+
def get_jackett_indexers(self, what: str) -> List[str]:
152+
params = urlencode([
152153
('apikey', self.api_key),
153154
('t', 'indexers'),
154155
('configured', 'true')
155-
]
156-
params = urlencode(params)
156+
])
157157
jacket_url = self.url + "/api/v2.0/indexers/all/results/torznab/api?%s" % params
158158
response = self.get_response(jacket_url)
159159
if response is None:
160160
self.handle_error("connection error getting indexer list", what)
161-
return
161+
return []
162162
# process results
163163
response_xml = xml.etree.ElementTree.fromstring(response)
164164
indexers = []
165165
for indexer in response_xml.findall('indexer'):
166166
indexers.append(indexer.attrib['id'])
167167
return indexers
168168

169-
def search_jackett_indexer(self, what, category, indexer_id):
169+
def search_jackett_indexer(self, what: str, category: Union[List[str], None], indexer_id: str) -> None:
170+
def toStr(s: Union[str, None]) -> str:
171+
return s if s is not None else ''
172+
173+
def getTextProp(e: Union[xml.etree.ElementTree.Element[str], None]) -> str:
174+
return toStr(e.text if e is not None else '')
175+
170176
# prepare jackett url
171-
params = [
177+
params_tmp = [
172178
('apikey', self.api_key),
173179
('q', what)
174180
]
175181
if category is not None:
176-
params.append(('cat', ','.join(category)))
177-
params = urlencode(params)
182+
params_tmp.append(('cat', ','.join(category)))
183+
params = urlencode(params_tmp)
178184
jacket_url = self.url + "/api/v2.0/indexers/" + indexer_id + "/results/torznab/api?%s" % params # noqa
179185
response = self.get_response(jacket_url)
180186
if response is None:
181187
self.handle_error("connection error for indexer: " + indexer_id, what)
182188
return
183189
# process search results
184190
response_xml = xml.etree.ElementTree.fromstring(response)
185-
for result in response_xml.find('channel').findall('item'):
186-
res = {}
191+
channel = response_xml.find('channel')
192+
if channel is None:
193+
return
194+
for result in channel.findall('item'):
195+
res: Dict[str, Any] = {}
187196

188-
title = result.find('title')
189-
if title is not None:
190-
title = title.text
197+
title_tmp = result.find('title')
198+
if title_tmp is not None:
199+
title = title_tmp.text
191200
else:
192201
continue
193202

194-
tracker = result.find('jackettindexer')
195-
tracker = '' if tracker is None else tracker.text
203+
tracker = getTextProp(result.find('jackettindexer'))
196204
if CONFIG_DATA['tracker_first']:
197205
res['name'] = '[%s] %s' % (tracker, title)
198206
else:
@@ -209,7 +217,7 @@ def search_jackett_indexer(self, what, category, indexer_id):
209217
continue
210218

211219
res['size'] = result.find('size')
212-
res['size'] = -1 if res['size'] is None else (res['size'].text + ' B')
220+
res['size'] = -1 if res['size'] is None else (toStr(res['size'].text) + ' B')
213221

214222
res['seeds'] = result.find(self.generate_xpath('seeders'))
215223
res['seeds'] = -1 if res['seeds'] is None else int(res['seeds'].attrib['value'])
@@ -231,17 +239,17 @@ def search_jackett_indexer(self, what, category, indexer_id):
231239
res['engine_url'] = self.url
232240

233241
try:
234-
date = datetime.strptime(result.find('pubDate').text, '%a, %d %b %Y %H:%M:%S %z')
242+
date = datetime.strptime(getTextProp(result.find('pubDate')), '%a, %d %b %Y %H:%M:%S %z')
235243
res['pub_date'] = int(date.timestamp())
236244
except Exception:
237245
res['pub_date'] = -1
238246

239247
self.pretty_printer_thread_safe(res)
240248

241-
def generate_xpath(self, tag):
249+
def generate_xpath(self, tag: str) -> str:
242250
return './{http://torznab.com/schemas/2015/feed}attr[@name="%s"]' % tag
243251

244-
def get_response(self, query):
252+
def get_response(self, query: str) -> Union[str, None]:
245253
response = None
246254
try:
247255
# we can't use helpers.retrieve_url because of redirects
@@ -256,24 +264,26 @@ def get_response(self, query):
256264
pass
257265
return response
258266

259-
def handle_error(self, error_msg, what):
267+
def handle_error(self, error_msg: str, what: str) -> None:
260268
# we need to print the search text to be displayed in qBittorrent when
261269
# 'Torrent names only' is enabled
262270
self.pretty_printer_thread_safe({
263-
'seeds': -1,
271+
'link': self.url,
272+
'name': "Jackett: %s! Right-click this row and select 'Open description page' to open help. Configuration file: '%s' Search: '%s'" % (error_msg, CONFIG_PATH, what), # noqa
264273
'size': -1,
274+
'seeds': -1,
265275
'leech': -1,
266276
'engine_url': self.url,
267-
'link': self.url,
268277
'desc_link': 'https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin', # noqa
269-
'name': "Jackett: %s! Right-click this row and select 'Open description page' to open help. Configuration file: '%s' Search: '%s'" % (error_msg, CONFIG_PATH, what) # noqa
278+
'pub_date': -1
270279
})
271280

272-
def pretty_printer_thread_safe(self, dictionary):
281+
def pretty_printer_thread_safe(self, dictionary: Dict[str, Any]) -> None:
282+
escaped_dict = self.escape_pipe(dictionary)
273283
with PRINTER_THREAD_LOCK:
274-
prettyPrinter(self.escape_pipe(dictionary))
284+
prettyPrinter(escaped_dict) # type: ignore[arg-type] # refactor later
275285

276-
def escape_pipe(self, dictionary):
286+
def escape_pipe(self, dictionary: Dict[str, Any]) -> Dict[str, Any]:
277287
# Safety measure until it's fixed in prettyPrinter
278288
for key in dictionary.keys():
279289
if isinstance(dictionary[key], str):

0 commit comments

Comments
 (0)