Skip to content

Commit 290e4fa

Browse files
committed
async_callback 추가 및 callback 관련 로직 변경
1 parent bc98378 commit 290e4fa

3 files changed

Lines changed: 95 additions & 12 deletions

File tree

WebtoonScraper/scrapers/_helpers.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ def register(self, scraper: Scraper) -> None:
2424
scraper.register_callback("finalize", self.finalizer)
2525

2626
def unregister(self, scraper: Scraper) -> None:
27-
with suppress(ValueError):
28-
scraper._triggers["initialize"].remove(self.initializer)
29-
with suppress(ValueError):
30-
scraper._triggers["finalize"].remove(self.finalizer)
27+
scraper.unregister_callback("initialize", self.initializer, "sync")
28+
scraper.unregister_callback("initialize", self.finalizer, "sync")
3129

3230
def initializer(self, scraper: Scraper, webtoon_directory: Path):
3331
pass

WebtoonScraper/scrapers/_scraper.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
import warnings
1111
from abc import abstractmethod
1212
from collections import defaultdict
13-
from collections.abc import Callable, Container, Iterator, Mapping
13+
from collections.abc import Callable, Container, Coroutine, Iterator, Mapping
1414
from contextlib import contextmanager, suppress
1515
from pathlib import Path
1616
from typing import (
1717
TYPE_CHECKING,
18+
Any,
1819
ClassVar,
1920
Generic,
2021
Literal,
@@ -24,14 +25,13 @@
2425
)
2526

2627
import filetype
28+
import httpc
2729
import httpx
2830
import pyfilename as pf
2931
from filetype.types import IMAGE
3032
from rich import progress
3133
from yarl import URL
3234

33-
import httpc
34-
3535
from ..base import console, logger, platforms
3636
from ..directory_state import (
3737
DirectoryState,
@@ -157,7 +157,6 @@ class Scraper(Generic[WebtoonId]): # MARK: SCRAPER
157157
PLATFORM: ClassVar[str]
158158
DOWNLOAD_INTERVAL: int | float = 0
159159
EXTRA_INFO_SCRAPER_FACTORY: type[ExtraInfoScraper] = ExtraInfoScraper
160-
TASK_QUEUE_FACTORY: Callable = asyncio.Queue
161160
information_vars: dict[str, None | str | Path | Callable] = dict(
162161
title=None,
163162
platform="PLATFORM",
@@ -207,8 +206,10 @@ def __init__(self, webtoon_id: WebtoonId) -> None:
207206
self.skip_download: list[int] = []
208207
"""0-based index를 사용해 다운로드를 생략할 웹툰을 결정합니다."""
209208
self._download_status: Literal["downloading", "nothing", "canceling"] = "nothing"
210-
self._triggers: defaultdict[str, list[Callable]] = defaultdict(list)
211-
self._tasks: asyncio.Queue[asyncio.Future] = self.TASK_QUEUE_FACTORY()
209+
# self._triggers: defaultdict[tuple[Literal["async", "async_task"], str], list[Callable[..., Coroutine]]] | defaultdict[tuple[Literal["sync"], str], list[Callable]] = defaultdict(list)
210+
# 적어도 pyright에서는 위의 type expr가 잘 작동하지 않음. 아래의 더 generic한 버전을 사용
211+
self._triggers: defaultdict[tuple[Literal["sync", "async", "async_task"], str], list[Callable[..., Coroutine]] | list[Callable]] = defaultdict(list)
212+
self._tasks: asyncio.Queue[asyncio.Future] = asyncio.Queue()
212213
"""_tasks에 값을 등록해 두면 스크래퍼가 종료될 때 해당 task들을 완료하거나 취소합니다."""
213214

214215
# initialize extra info scraper
@@ -386,6 +387,49 @@ async def fetch_all(self, reload: bool = False) -> None:
386387
await self.fetch_webtoon_information(reload=reload)
387388
await self.fetch_episode_information(reload=reload)
388389

390+
@overload
391+
def register_async_callback(self, trigger: str, func: CallableT, *, blocking: bool = True) -> CallableT: ...
392+
393+
@overload
394+
def register_async_callback(self, trigger: str, *, blocking: bool = True) -> Callable[[CallableT], CallableT]: ...
395+
396+
def register_async_callback(self, trigger: str, func: Callable[..., Coroutine] | None = None, *, blocking: bool = True) -> Any:
397+
"""특정 callback 트리거가 발생했을 때 실행할 비동기 콜백을 등록합니다."""
398+
if func is None:
399+
return lambda func: self.register_async_callback(trigger, func, blocking=blocking)
400+
401+
# blocking으로 할지 말지를 callback을 등록할 때 해야 할까, 아님 부를 때 결정해야 할까?
402+
# 실례를 한번 봐야 할 것 같은데 아직은 잘 모르겠다.
403+
# 일단 지금은 callback을 등록할 때 결정하는 것으로 한다.
404+
self._triggers[("async" if blocking else "async_task", trigger)].append(func)
405+
return func
406+
407+
async def async_callback(self, situation: str, **context) -> list[asyncio.Task] | None:
408+
if callbacks := self._triggers.get(("async", situation)):
409+
for callback in callbacks:
410+
await callback(scraper=self, **context)
411+
412+
if callbacks := self._triggers.get(("async_task", situation)):
413+
tasks = []
414+
print("starting task!")
415+
for callback in callbacks:
416+
task = asyncio.create_task(callback(scraper=self, **context))
417+
await self._tasks.put(task)
418+
print("I got task!")
419+
tasks.append(task)
420+
return tasks or None
421+
422+
self.callback(situation, **context)
423+
424+
def unregister_callback(self, trigger: str, func: Callable, type: Literal["sync", "async", "async_task"] | None = None) -> None:
425+
# 굳이 빈 key를 만들 필욘 없으니 get을 사용. 그냥 [] 사용해도 솔직히 상관없음.
426+
if type == "sync" or type is None:
427+
self._triggers.get(("sync", trigger), []).remove(func)
428+
if type == "async" or type is None:
429+
self._triggers.get(("async", trigger), []).remove(func)
430+
if type == "async_task" or type is None:
431+
self._triggers.get(("async_task", trigger), []).remove(func)
432+
389433
@overload
390434
def register_callback(self, trigger: str, func: CallableT) -> CallableT: ...
391435

@@ -424,7 +468,7 @@ def startup_message(scraper: Scraper, finishing: bool, **context):
424468
if func is None:
425469
return lambda func: self.register_callback(trigger, func)
426470

427-
self._triggers[trigger].append(func)
471+
self._triggers[("sync", trigger)].append(func)
428472
return func
429473

430474
def callback(self, situation: str, **context) -> None:
@@ -496,7 +540,7 @@ def callback(self, situation: str, **context) -> None:
496540
case the_others, _:
497541
logger.debug(f"WebtoonScraper status: {the_others}")
498542

499-
if callbacks := self._triggers.get(situation):
543+
if callbacks := self._triggers.get(("sync", situation)):
500544
for callback in callbacks:
501545
callback(scraper=self, **context)
502546

tests/test_scrapers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import pytest
23
from WebtoonScraper.scrapers import * # type: ignore
34

@@ -47,3 +48,43 @@ def test_from_url():
4748
assert LezhinComicsScraper.from_url(
4849
"https://www.lezhin.com/ko/comic/dr_hearthstone"
4950
).webtoon_id == "dr_hearthstone"
51+
52+
53+
def test_callback():
54+
asyncio.run(async_test_callback())
55+
56+
57+
async def async_test_callback():
58+
scraper = NaverWebtoonScraper.from_url(
59+
"https://comic.naver.com/webtoon/list?titleId=805702"
60+
)
61+
62+
@scraper.register_async_callback("async_trigger")
63+
async def async_callback(scraper, **context):
64+
assert context["key"] == "value"
65+
66+
@scraper.register_async_callback("async_task_trigger", blocking=False)
67+
async def async_callback_task(scraper, **context):
68+
assert context["key"] == "value"
69+
return "return_value"
70+
71+
@scraper.register_callback("trigger")
72+
def callback(scraper, **context):
73+
assert context["key"] == "value"
74+
75+
await scraper.async_callback("async_trigger", key="value")
76+
await scraper.async_callback("trigger", key="value")
77+
scraper.callback("trigger", key="value")
78+
task, = await scraper.async_callback("async_task_trigger", key="value") # type: ignore
79+
assert await task == "return_value"
80+
81+
with pytest.raises(AssertionError):
82+
await scraper.async_callback("async_trigger", key="not_a_value")
83+
with pytest.raises(AssertionError):
84+
await scraper.async_callback("trigger", key="not_a_value")
85+
scraper.callback("async_trigger", key="not_a_value")
86+
with pytest.raises(AssertionError):
87+
scraper.callback("trigger", key="not_a_value")
88+
with pytest.raises(AssertionError):
89+
task, = await scraper.async_callback("async_task_trigger", key="not_a_value") # type: ignore
90+
await task

0 commit comments

Comments
 (0)