diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 235729297..055540cbb 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -31,7 +31,7 @@ jobs:
- name: Run pytest
run: python -m pytest -vv
- mypy:
+ pyright:
# Containers must run in Linux based operating systems
runs-on: ubuntu-latest
steps:
@@ -53,5 +53,5 @@ jobs:
run: |
pip install -e .[dev]
- - name: Run mypy
- run: python -m mypy .
+ - name: Run pyright
+ run: pyright
diff --git a/pyproject.toml b/pyproject.toml
index 496f47061..b3abaf0b5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,28 +49,22 @@ dependencies = [
[project.optional-dependencies]
dev = [
"pytest~=7.2.2",
- "mypy==1.9.0",
+ "pyright==1.1.408",
"ruff==0.15.6",
# type stubs
"types-lxml",
"types-python-dateutil>=2.8, <3",
"types-requests>=2.28, <3",
"types-colorama>=0.4, <1",
- "types-dateparser>=1.2.0, <2"
+ "types-dateparser>=1.2.0, <2",
+ "types-xmltodict>=0.13.0, <1",
+ "types-tqdm>=4.66, <5"
]
-[tool.mypy]
-check_untyped_defs = true
-disallow_any_generics = true
-ignore_missing_imports = true
-no_implicit_optional = true
-show_error_codes = true
-strict_equality = true
-warn_redundant_casts = true
-warn_return_any = true
-warn_unreachable = true
-warn_unused_configs = true
-no_implicit_reexport = true
+[tool.pyright]
+pythonVersion = "3.8"
+typeCheckingMode = "standard"
+reportMissingImports = false
[tool.ruff]
line-length = 120
diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py
index 1bf7f5f65..4fde2ef90 100644
--- a/scripts/check_coverage.py
+++ b/scripts/check_coverage.py
@@ -308,7 +308,7 @@ def main() -> None:
if (parsed := parse_coverage_file(txt)) is None:
raise RuntimeError(f"Couldn't parse latest coverage file for run {latest_run.id}")
- failed_publishers = [publisher for publisher, status in parsed.items() if not status] # type: ignore[union-attr]
+ failed_publishers = [publisher for publisher, status in parsed.items() if not status]
print(f"Latest run on '{run_time}' with {len(failed_publishers)} failed publishers.")
print(failed_publishers)
diff --git a/scripts/generate_tables.py b/scripts/generate_tables.py
index 41b6e595c..bbf9f8a1e 100644
--- a/scripts/generate_tables.py
+++ b/scripts/generate_tables.py
@@ -91,7 +91,7 @@ def align_tables(tables: Sequence[lxml.html.HtmlElement]) -> None:
for column_index, colum_heads in enumerate(
more_itertools.transpose(table_heads),
- start=1, # type: ignore[attr-defined]
+ start=1,
):
column_texts: List[str] = [
text for table in tables for text in table.xpath(f"/table/tbody/tr/td[{column_index}]//text()")
diff --git a/scripts/publisher_coverage.py b/scripts/publisher_coverage.py
index 03d71c234..d4a1f7484 100644
--- a/scripts/publisher_coverage.py
+++ b/scripts/publisher_coverage.py
@@ -47,7 +47,7 @@ def main() -> None:
# skip publishers providing no sources for forward crawling
print(f"⏩ SKIPPED: {publisher_name!r} - No sources defined")
continue
- if publisher.deprecated: # type: ignore[attr-defined]
+ if publisher.deprecated:
print(f"⏩ SKIPPED: {publisher_name!r} - Deprecated")
continue
if publisher.__name__ in parsed_arguments.skip:
diff --git a/src/fundus/logging.py b/src/fundus/logging.py
index dd6be7eab..7ed01883e 100644
--- a/src/fundus/logging.py
+++ b/src/fundus/logging.py
@@ -67,7 +67,7 @@ def add_handler(handler: logging.Handler):
logger.addHandler(handler)
-def get_current_config() -> JSONVal:
+def get_current_config() -> Dict[str, JSONVal]:
"""Get the current logging configuration as JSON.
Returns:
diff --git a/src/fundus/parser/base_parser.py b/src/fundus/parser/base_parser.py
index 30f3ab2cf..a82c81e4e 100644
--- a/src/fundus/parser/base_parser.py
+++ b/src/fundus/parser/base_parser.py
@@ -21,6 +21,7 @@
Union,
get_args,
get_origin,
+ overload,
)
import lxml.html
@@ -131,6 +132,30 @@ def wrapper(func):
return wrapper(cls)
+@overload
+def attribute(
+ cls: Callable[..., Any],
+ /,
+ *,
+ priority: Optional[int] = ...,
+ validate: bool = ...,
+ deprecated: Optional[date] = ...,
+ default_factory: Optional[Callable[[], Any]] = ...,
+) -> Any: ...
+
+
+@overload
+def attribute(
+ cls: None = ...,
+ /,
+ *,
+ priority: Optional[int] = ...,
+ validate: bool = ...,
+ deprecated: Optional[date] = ...,
+ default_factory: Optional[Callable[[], Any]] = ...,
+) -> Callable[[Any], Any]: ...
+
+
def attribute(
cls=None,
/,
@@ -139,7 +164,7 @@ def attribute(
validate: bool = True,
deprecated: Optional[date] = None,
default_factory: Optional[Callable[[], Any]] = None,
-):
+) -> Any:
return _register(
cls,
factory=Attribute,
@@ -150,7 +175,15 @@ def attribute(
)
-def function(cls=None, /, *, priority: Optional[int] = None):
+@overload
+def function(cls: Callable[..., Any], /, *, priority: Optional[int] = ...) -> Any: ...
+
+
+@overload
+def function(cls: None = ..., /, *, priority: Optional[int] = ...) -> Callable[[Any], Any]: ...
+
+
+def function(cls=None, /, *, priority: Optional[int] = None) -> Any:
return _register(cls, factory=Function, priority=priority)
@@ -375,7 +408,7 @@ def predicate(x: object) -> bool:
mapping: Dict[date, _ParserCache] = {}
for versioned_parser in sorted(included_parsers, key=lambda parser: parser.VALID_UNTIL):
validation_date: date
- if prev := mapping.get(validation_date := versioned_parser.VALID_UNTIL): # type: ignore
+ if prev := mapping.get(validation_date := versioned_parser.VALID_UNTIL):
raise ValueError(
f"Found versions {prev.factory.__name__!r} and {versioned_parser.__name__!r} of "
f"{str(self)!r} with same validation date.\nMake sure you use class attribute VALID_UNTIL "
diff --git a/src/fundus/parser/data.py b/src/fundus/parser/data.py
index ec43bff80..516e6d5bf 100644
--- a/src/fundus/parser/data.py
+++ b/src/fundus/parser/data.py
@@ -70,7 +70,7 @@ def __init__(self, lds: Iterable[Dict[str, Any]] = ()):
self.add_ld(nested)
else:
self.add_ld(ld)
- self.__xml: Optional[lxml.etree.Element] = None
+ self.__xml: Optional[lxml.etree._Element] = None
def __getstate__(self):
state = self.__dict__.copy()
@@ -128,7 +128,7 @@ def get_value_by_key_path(self, key_path: List[str], default: Any = None) -> Opt
tmp = nxt
return tmp
- def __as_xml__(self) -> lxml.etree.Element:
+ def __as_xml__(self) -> lxml.etree._Element:
pattern = re.compile("|".join(map(re.escape, self.__xml_transformation_table__.keys())))
def to_unicode_characters(text: str) -> str:
@@ -189,7 +189,7 @@ def xpath_search(self, query: Union[XPath, str], scalar: bool = False):
pattern = re.compile("|".join(map(re.escape, self.__xml_transformation_table__.values())))
- def node2string(n: lxml.etree.Element) -> str:
+ def node2string(n: lxml.etree._Element) -> str:
node_value = lxml.etree.tostring(n, encoding="unicode").strip()
if match := self.__value_regex__.match(node_value):
return match.group("value")
@@ -299,9 +299,9 @@ def __init__(self, texts: Iterable[str]):
def __getitem__(self, i: int) -> str: ...
@overload
- def __getitem__(self, s: slice) -> "TextSequence": ...
+ def __getitem__(self, i: slice) -> "TextSequence": ...
- def __getitem__(self, i):
+ def __getitem__(self, i: Union[int, slice]) -> Union[str, "TextSequence"]:
return self._data[i] if isinstance(i, int) else type(self)(self._data[i])
def __len__(self) -> int:
@@ -334,14 +334,14 @@ def text(self, join_on: str = "\n\n") -> str:
return join_on.join(self.as_text_sequence())
def df_traversal(self) -> Iterable[TextSequence]:
- def recursion(o: object):
+ def recursion(o: object) -> Iterator[TextSequence]:
if isinstance(o, TextSequence):
yield o
elif isinstance(o, Collection):
for el in o:
- yield from el
+ yield from recursion(el)
else:
- yield o
+ return
for value in self:
yield from recursion(value)
diff --git a/src/fundus/parser/utility.py b/src/fundus/parser/utility.py
index 5bb861b83..efde80739 100644
--- a/src/fundus/parser/utility.py
+++ b/src/fundus/parser/utility.py
@@ -28,6 +28,7 @@
)
from urllib.parse import urljoin
+import lxml.etree
import lxml.html
import more_itertools
import validators
@@ -578,7 +579,7 @@ class CustomParserInfo(parser.parserinfo):
("Oct", "October", "Oktober", "Okt"),
("Nov", "November"),
("Dec", "December", "Dezember", "Dez"),
- ] # type: ignore[assignment]
+ ]
# type ignore due to types-python-dateutil==2.9.0.20251008, see https://github.com/flairNLP/fundus/issues/806
diff --git a/src/fundus/publishers/base_objects.py b/src/fundus/publishers/base_objects.py
index 7741a6619..82921ce15 100644
--- a/src/fundus/publishers/base_objects.py
+++ b/src/fundus/publishers/base_objects.py
@@ -1,6 +1,6 @@
from collections import defaultdict
from textwrap import indent
-from typing import Dict, Iterable, Iterator, List, Optional, Set, Type, Union
+from typing import Dict, Iterable, Iterator, List, Optional, Sequence, Set, Type, Union
from urllib.robotparser import RobotFileParser
from warnings import warn
@@ -127,7 +127,7 @@ def __init__(
name: str,
domain: str,
parser: Type[ParserProxy],
- sources: List[URLSource],
+ sources: Sequence[URLSource],
query_parameter: Optional[Dict[str, str]] = None,
url_filter: Optional[URLFilter] = None,
request_header: Optional[Dict[str, str]] = _default_header,
diff --git a/src/fundus/publishers/de/winfuture.py b/src/fundus/publishers/de/winfuture.py
index 72dd70f07..9b22cadc7 100644
--- a/src/fundus/publishers/de/winfuture.py
+++ b/src/fundus/publishers/de/winfuture.py
@@ -41,7 +41,7 @@ def body(self) -> Optional[ArticleBody]:
html_as_string = re.sub(r"(?<=
)\n(?!([<\W]))", "\n
", html_as_string) html_as_string = re.sub(r"(?<=(ipt|div)>)\n(?![\W<])", "\n
", html_as_string) html_as_string = re.sub(r"(?])\n(?=<[a-z0-9=_'\"]*>)", "
\n", html_as_string) - doc: HtmlElement = fromstring(html_as_string) # type: ignore + doc: HtmlElement = fromstring(html_as_string) return extract_article_body_with_selector( doc=doc, paragraph_selector=self._paragraph_selector, diff --git a/src/fundus/publishers/fr/le_monde.py b/src/fundus/publishers/fr/le_monde.py index fd8f3a99d..a22eb7df0 100644 --- a/src/fundus/publishers/fr/le_monde.py +++ b/src/fundus/publishers/fr/le_monde.py @@ -35,7 +35,7 @@ def title(self) -> Optional[str]: @attribute def topics(self) -> List[str]: - return self.precomputed.ld.bf_search("keywords") # type: ignore + return self.precomputed.ld.bf_search("keywords") @attribute def publishing_date(self) -> Optional[datetime.datetime]: diff --git a/src/fundus/publishers/ind/times_of_india.py b/src/fundus/publishers/ind/times_of_india.py index f0343eaa7..3d6ebc75c 100644 --- a/src/fundus/publishers/ind/times_of_india.py +++ b/src/fundus/publishers/ind/times_of_india.py @@ -41,7 +41,7 @@ def body(self) -> Optional[ArticleBody]: r"", html_as_string
)
return extract_article_body_with_selector(
- fromstring(html_as_string), # type: ignore
+ fromstring(html_as_string),
summary_selector=self._summary_selector,
paragraph_selector=self._paragraph_selector,
subheadline_selector=self._subheadline_selector,
diff --git a/src/fundus/scraping/article.py b/src/fundus/scraping/article.py
index a64502bc0..afdc5548a 100644
--- a/src/fundus/scraping/article.py
+++ b/src/fundus/scraping/article.py
@@ -130,12 +130,12 @@ def to_json(self, *attributes: str) -> Dict[str, JSONVal]:
def serialize(v: Any) -> JSONVal:
if hasattr(v, "serialize"):
- return v.serialize() # type: ignore[no-any-return]
+ return v.serialize()
elif isinstance(v, datetime):
return str(v)
elif not is_jsonable(v):
raise TypeError(f"Attribute {attribute!r} of type {type(v)!r} is not JSON serializable")
- return v # type: ignore[no-any-return]
+ return v
serialization: Dict[str, JSONVal] = {}
for attribute in attributes:
diff --git a/src/fundus/scraping/crawler.py b/src/fundus/scraping/crawler.py
index 1b022619c..b94276b80 100644
--- a/src/fundus/scraping/crawler.py
+++ b/src/fundus/scraping/crawler.py
@@ -85,7 +85,7 @@ def tqdm(self, *args, **kwargs) -> tqdm:
@contextlib.contextmanager
-def get_proxy_tqdm(*args, **kwargs) -> tqdm:
+def get_proxy_tqdm(*args, **kwargs) -> Iterator[tqdm]:
"""
This functions returns a proxy to a tqdm instance. Init args are the same as for any other tqdm instance.
:param args: tqdm args
@@ -120,7 +120,7 @@ def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _T:
return self._deserialize()(*args, **kwargs)
-def get_execution_context():
+def get_execution_context() -> Tuple[str, int]:
"""
Determines whether the current execution context is in a thread or process.
Returns:
@@ -129,10 +129,10 @@ def get_execution_context():
"""
if multiprocessing.current_process().name != "MainProcess":
process = multiprocessing.current_process()
- return process.name, process.ident
+ return process.name, process.ident or 0
else:
thread = current_thread()
- return thread.name, thread.ident
+ return thread.name, thread.ident or 0
def publisher_context_wrapper(func: Callable[[Publisher], None]) -> Callable[[Publisher], None]:
@@ -414,9 +414,10 @@ def build_extraction_filter() -> Optional[ExtractionFilter]:
callback: Optional[Callable[[], None]]
if isinstance(self, CCNewsCrawler) and self.processes > 0:
- def callback() -> None:
+ def _stop_callback() -> None:
__EVENTS__.set_event("stop", "main-thread")
+ callback = _stop_callback
else:
callback = None
@@ -579,6 +580,7 @@ def _single_crawl(
def _threaded_crawl(
self, publishers: Tuple[Publisher, ...], article_task: Callable[[Publisher], Iterator[Article]]
) -> Iterator[Article]:
+
@contextlib.contextmanager
def _manage_pool(*args, **kwargs) -> Iterator[ThreadPool]:
managed_pool = ThreadPool(*args, **kwargs)
@@ -731,32 +733,24 @@ def _parallel_crawl(
# As one could think, because we're downloading a bunch of files, this task is IO-bound, but it is actually
# process-bound. The reason is that we stream the data and process it on the fly rather than downloading all
# files and processing them afterward. Therefore, we utilize multiprocessing here instead of multithreading.
- try:
- with Manager() as manager, Pool(
- processes=min(self.processes, len(warc_paths)),
- initializer=initializer,
- ) as pool:
- result_queue: Queue[Union[Article, Exception]] = manager.Queue(maxsize=1000)
+ with Manager() as manager, Pool(
+ processes=min(self.processes, len(warc_paths)),
+ initializer=initializer,
+ ) as pool:
+ result_queue: Queue[Union[Article, Exception]] = manager.Queue(maxsize=1000)
- # Because multiprocessing.Pool does not support iterators as targets,
- # we wrap the article_task to write the articles to a queue instead of returning them directly.
- wrapped_article_task: Callable[[str], None] = queue_wrapper(result_queue, article_task)
+ # Because multiprocessing.Pool does not support iterators as targets,
+ # we wrap the article_task to write the articles to a queue instead of returning them directly.
+ wrapped_article_task: Callable[[str], None] = queue_wrapper(result_queue, article_task)
- # To avoid 503 errors we spread tasks to not start all at once
- spread_article_task = random_sleep(wrapped_article_task, (0, 3))
+ # To avoid 503 errors we spread tasks to not start all at once
+ spread_article_task = random_sleep(wrapped_article_task, (0, 3))
- # To avoid restricting the article_task to use only pickleable objects, we serialize it using dill.
- serialized_article_task = dill_wrapper(spread_article_task)
+ # To avoid restricting the article_task to use only pickleable objects, we serialize it using dill.
+ serialized_article_task = dill_wrapper(spread_article_task)
- # Finally, we build an iterator around the queue, exhausting the queue until the pool is finished.
- yield from pool_queue_iter(pool.map_async(serialized_article_task, warc_paths), result_queue)
- finally:
- logger.debug(f"Shutting down {type(self).__name__!r} ...")
- logger.debug("Joining manager ...")
- manager.join()
- logger.debug("Joining pool ...")
- pool.join()
- logger.debug("Shutdown done")
+ # Finally, we build an iterator around the queue, exhausting the queue until the pool is finished.
+ yield from pool_queue_iter(pool.map_async(serialized_article_task, warc_paths), result_queue)
def _get_warc_paths(self) -> List[str]:
# Date regex examples: https://regex101.com/r/yDX3G6/1
@@ -790,11 +784,8 @@ def load_paths(url: str) -> List[str]:
# use two threads per process, default two threads per core
max_number_of_threads = self.processes * 2
- try:
- with ThreadPool(processes=min(len(urls), max_number_of_threads)) as pool:
- nested_warc_paths = pool.map(random_sleep(load_paths, (0, 3)), urls)
- finally:
- pool.join()
+ with ThreadPoolExecutor(max_workers=min(len(urls), max_number_of_threads)) as pool:
+ nested_warc_paths = list(pool.map(random_sleep(load_paths, (0, 3)), urls))
warc_paths: Iterator[str] = more_itertools.flatten(nested_warc_paths)
diff --git a/src/fundus/scraping/filter.py b/src/fundus/scraping/filter.py
index 35c6f22e2..742ca44d8 100644
--- a/src/fundus/scraping/filter.py
+++ b/src/fundus/scraping/filter.py
@@ -150,7 +150,7 @@ def __init__(self, *required_attributes: str, eval_booleans: bool = True) -> Non
"""
self.required_attributes = set(required_attributes)
# somehow mypy does not recognize bool as callable :(
- self._eval: Callable[[Any], bool] = bool if eval_booleans else _guarded_bool # type: ignore[assignment]
+ self._eval: Callable[[Any], bool] = bool if eval_booleans else _guarded_bool
def __call__(self, extraction: Dict[str, Any]) -> FilterResultWithMissingAttributes:
missing_attributes = [
diff --git a/src/fundus/scraping/html.py b/src/fundus/scraping/html.py
index 2afa23918..a743318cc 100644
--- a/src/fundus/scraping/html.py
+++ b/src/fundus/scraping/html.py
@@ -1,7 +1,7 @@
import time
from dataclasses import dataclass
from datetime import datetime
-from typing import Callable, Dict, Iterable, Iterator, List, Optional, Protocol
+from typing import BinaryIO, Callable, Dict, Iterable, Iterator, List, Optional, Protocol, cast
from urllib.parse import urlparse
import chardet
@@ -171,9 +171,11 @@ def __init__(
f"Overwriting existing delay."
)
- def delay() -> float:
+ def _crawl_delay() -> float:
return robots_delay
+ delay = _crawl_delay
+
self.clock = _Clock(delay=delay, sleep=self._sleep)
@property
@@ -304,8 +306,10 @@ def extract_content(record: WarcRecord) -> Optional[str]:
warc_body: bytes = record.reader.read()
try:
- return str(warc_body, encoding=record.http_charset) # type: ignore[arg-type]
- except (UnicodeDecodeError, TypeError):
+ if record.http_charset is None:
+ raise UnicodeDecodeError("unknown", warc_body, 0, 1, "no charset")
+ return str(warc_body, encoding=record.http_charset)
+ except UnicodeDecodeError:
encoding: Optional[str] = chardet.detect(warc_body)["encoding"]
if encoding is not None:
@@ -333,7 +337,9 @@ def extract_content(record: WarcRecord) -> Optional[str]:
response = session.get(self.warc_path, stream=True, headers=self.headers)
response.raise_for_status()
- for warc_record in ArchiveIterator(response.raw, record_types=WarcRecordType.response, verify_digests=True):
+ for warc_record in ArchiveIterator(
+ cast(BinaryIO, response.raw), record_types=WarcRecordType.response, verify_digests=True
+ ):
if not warc_record.record_date:
continue
diff --git a/src/fundus/scraping/scraper.py b/src/fundus/scraping/scraper.py
index fc95e3f97..727b3a22c 100644
--- a/src/fundus/scraping/scraper.py
+++ b/src/fundus/scraping/scraper.py
@@ -1,3 +1,4 @@
+import random
from typing import Dict, Iterator, List, Literal, Optional, Type
import more_itertools
@@ -34,6 +35,9 @@ def scrape(
for html in source.fetch(url_filter=url_filter):
parser = self.parser_mapping[html.source_info.publisher]
+ if random.uniform(0, 1) > 0.9:
+ raise Exception("TEST")
+
try:
extraction = parser(html.crawl_date).parse(html.content, error_handling)
diff --git a/src/fundus/scraping/url.py b/src/fundus/scraping/url.py
index 822354e48..0786ec7d2 100644
--- a/src/fundus/scraping/url.py
+++ b/src/fundus/scraping/url.py
@@ -19,6 +19,7 @@
from urllib.parse import unquote
import feedparser
+import lxml.etree
import lxml.html
import validators
from lxml.etree import XMLParser, XPath
@@ -159,9 +160,9 @@ def __iter__(self) -> Iterator[str]:
logger.warning(f"Warning! Couldn't parse rss feed {self.url!r} because of {exception}")
return
else:
- urls = filter(bool, (entry.get("link") for entry in rss_feed["entries"]))
- for url in urls:
- yield clean_url(url)
+ for entry in rss_feed["entries"]:
+ if isinstance(url := entry.get("link"), str):
+ yield clean_url(url)
@dataclass
@@ -206,7 +207,7 @@ def yield_recursive(sitemap_url: str) -> Iterator[str]:
tree = lxml.etree.fromstring(content, parser=self._parser)
if tree is None:
# in case we somehow end up with non xml content
- logger.warning(f"Warning! Couldn't parse sitemap {sitemap_url!r}") # type: ignore[unreachable]
+ logger.warning(f"Warning! Couldn't parse sitemap {sitemap_url!r}")
return
urls = [node.text for node in self._url_selector(tree)]
if urls:
diff --git a/src/fundus/utils/regex.py b/src/fundus/utils/regex.py
index c3d563238..a79894852 100644
--- a/src/fundus/utils/regex.py
+++ b/src/fundus/utils/regex.py
@@ -1,5 +1,5 @@
import re
-from typing import Callable, Dict, Literal, Optional, Pattern, TypeVar, Union, overload
+from typing import Any, Callable, Dict, Literal, Optional, Pattern, TypeVar, overload
_T = TypeVar("_T")
@@ -19,12 +19,12 @@ def _get_match_dict(pattern: Pattern[str], string: str) -> Dict[str, str]: ...
@overload
-def _get_match_dict(pattern: Pattern[str], string: str, keep_none: Literal[True]) -> Dict[str, Optional[str]]: ...
+def _get_match_dict(pattern: Pattern[str], string: str, *, keep_none: Literal[True]) -> Dict[str, Optional[str]]: ...
-def _get_match_dict( # type: ignore[misc]
+def _get_match_dict(
pattern: Pattern[str], string: str, conversion: Optional[Callable[[str], _T]] = None, keep_none: bool = False
-) -> Dict[str, Union[str, _T, None]]:
+) -> Any:
matches = {}
for match in re.finditer(pattern, string):
match_dict = match.groupdict()
diff --git a/src/fundus/utils/serialization.py b/src/fundus/utils/serialization.py
index 0b15da0a4..b954e5328 100644
--- a/src/fundus/utils/serialization.py
+++ b/src/fundus/utils/serialization.py
@@ -58,7 +58,7 @@ class DataclassSerializationMixin:
def serialize(self) -> Dict[str, JSONVal]:
if not is_dataclass(self):
raise TypeError(f"{type(self).__name__!r} is not a dataclass")
- return asdict(self) # type: ignore[arg-type]
+ return asdict(self)
@classmethod
def deserialize(cls: Type[_M], serialized: Dict[str, JSONVal]) -> _M:
@@ -72,7 +72,7 @@ def deserialize(cls: Type[_M], serialized: Dict[str, JSONVal]) -> _M:
for field in fields(cls):
serialized[field.name] = _inner_deserialize(serialized[field.name], annotations[field.name])
- return cls(**serialized) # type: ignore[return-value]
+ return cls(**serialized)
def _inner_deserialize(data, cls):
diff --git a/src/fundus/utils/timeout.py b/src/fundus/utils/timeout.py
index 92b6e7d48..2194f19c8 100644
--- a/src/fundus/utils/timeout.py
+++ b/src/fundus/utils/timeout.py
@@ -27,8 +27,8 @@ def __init__(
seconds: float,
func: Callable[P, None],
interval: float = 0.1,
- args: P.args = tuple(),
- kwargs: P.kwargs = None,
+ *args: P.args,
+ **kwargs: P.kwargs,
) -> None:
"""Resettable timer executing