all: add type checking with Mypy
- ID
9246d0f- date
2025-10-19 06:17:55+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
4b36185- message
all: add type checking with Mypy Fixes #11- changed files
6 files, 45 additions, 28 deletions
Changed files
.github/workflows/test.yml (897) → .github/workflows/test.yml (952)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1e179b3..fdf2bb9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -36,6 +36,9 @@ jobs:
ruff check .
ruff format --check .
+ - name: Check types
+ run: mypy *.py --strict
+
# I don't run tests in GitHub Actions because YouTube blocks yt-dlp
# without authentication, and I don't want to sort out passing cookies
# in CI for now -- I can run the tests locally, that's good enough.
dev_requirements.in (33) → dev_requirements.in (51)
diff --git a/dev_requirements.in b/dev_requirements.in
index 368464a..6737fd3 100644
--- a/dev_requirements.in
+++ b/dev_requirements.in
@@ -1,4 +1,6 @@
-r requirements.txt
+mypy
pytest
ruff
+types-yt-dlp
dev_requirements.txt (1016) → dev_requirements.txt (1270)
diff --git a/dev_requirements.txt b/dev_requirements.txt
index b22ca4c..8a8d1cf 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -6,22 +6,28 @@ certifi==2025.10.5
# via
# -r requirements.txt
# requests
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via
# -r requirements.txt
# requests
-gallery-dl==1.30.9
+gallery-dl==1.30.10
# via -r requirements.txt
-idna==3.10
+idna==3.11
# via
# -r requirements.txt
# requests
-iniconfig==2.1.0
+iniconfig==2.3.0
# via pytest
mutagen==1.47.0
# via -r requirements.txt
+mypy==1.18.2
+ # via -r dev_requirements.in
+mypy-extensions==1.1.0
+ # via mypy
packaging==25.0
# via pytest
+pathspec==0.12.1
+ # via mypy
pluggy==1.6.0
# via pytest
pycryptodomex==3.23.0
@@ -34,13 +40,19 @@ requests==2.32.5
# via
# -r requirements.txt
# gallery-dl
-ruff==0.13.3
+ruff==0.14.1
+ # via -r dev_requirements.in
+types-yt-dlp==2025.9.26.20251009
# via -r dev_requirements.in
+typing-extensions==4.15.0
+ # via mypy
urllib3==2.5.0
# via
# -r requirements.txt
# requests
websockets==15.0.1
- # via -r requirements.txt
-yt-dlp==2025.9.26
+ # via
+ # -r requirements.txt
+ # types-yt-dlp
+yt-dlp==2025.10.14
# via -r requirements.txt
requirements.txt (618) → requirements.txt (620)
diff --git a/requirements.txt b/requirements.txt
index 946486c..5f4e911 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,11 +6,11 @@ certifi==2025.10.5
# via
# requests
# yt-dlp
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
# via requests
-gallery-dl==1.30.9
+gallery-dl==1.30.10
# via -r requirements.in
-idna==3.10
+idna==3.11
# via requests
mutagen==1.47.0
# via yt-dlp
@@ -26,5 +26,5 @@ urllib3==2.5.0
# yt-dlp
websockets==15.0.1
# via yt-dlp
-yt-dlp==2025.9.26
+yt-dlp==2025.10.14
# via -r requirements.in
tests/test_yt-dlp_alexwlchan.py (1505) → test_yt-dlp_alexwlchan.py (1508)
diff --git a/tests/test_yt-dlp_alexwlchan.py b/test_yt-dlp_alexwlchan.py
similarity index 82%
rename from tests/test_yt-dlp_alexwlchan.py
rename to test_yt-dlp_alexwlchan.py
index 9e5c234..5b28cf2 100644
--- a/tests/test_yt-dlp_alexwlchan.py
+++ b/test_yt-dlp_alexwlchan.py
@@ -1,9 +1,10 @@
import json
import os
import subprocess
+from typing import Any
-def download_video(url):
+def download_video(url: str) -> Any:
output = subprocess.check_output(["python3", "yt-dlp_alexwlchan.py", url])
video_info = json.loads(output)
@@ -25,7 +26,7 @@ def test_youtube_video() -> None:
assert video_info["subtitle_path"] is None
assert video_info["id"] == "TUQaGhPdlxs"
- assert video_info["date_uploaded"] == "2008-04-19T03:51:21Z"
+ assert video_info["date_uploaded"] == "2022-03-25T01:10:38Z"
def test_instagram_video() -> None:
@@ -40,10 +41,7 @@ def test_instagram_video() -> None:
assert video_info["channel"]["id"] == "52716733233"
assert video_info["channel"]["name"] == "Public Domain Gems"
- assert (
- video_info["channel"]["channel_url"]
- == "https://www.instagram.com/publicdomaingems/"
- )
+ assert video_info["channel"]["url"] == "https://www.instagram.com/publicdomaingems/"
assert video_info["id"] == "DMWY8KkOS0n"
- assert video_info["date_uploaded"] == "2008-04-19T03:51:21Z"
+ assert video_info["date_uploaded"] == "2025-07-21T00:34:41Z"
yt-dlp_alexwlchan.py (4890) → yt-dlp_alexwlchan.py (4959)
diff --git a/yt-dlp_alexwlchan.py b/yt-dlp_alexwlchan.py
index b7b485b..ce6f547 100755
--- a/yt-dlp_alexwlchan.py
+++ b/yt-dlp_alexwlchan.py
@@ -6,12 +6,12 @@ from pathlib import Path
import subprocess
import sys
import tempfile
-from typing import TypedDict
+from typing import Any, TypedDict
from yt_dlp import YoutubeDL
-ydl_opts = {
+ydl_opts: Any = {
# Print progress output to stderr, not stdout
"logtostderr": True,
#
@@ -44,7 +44,7 @@ def get_youtube_avatar_url(channel_url: str) -> str:
"""
Returns the avatar URL of a YouTube channel.
"""
- ydl_opts = {
+ ydl_opts: Any = {
# Print progress output to stderr, not stdout
"logtostderr": True,
#
@@ -57,11 +57,11 @@ def get_youtube_avatar_url(channel_url: str) -> str:
}
with YoutubeDL(ydl_opts) as ydl:
- channel_info = ydl.extract_info(channel_url, download=False)
+ channel_info: Any = ydl.extract_info(channel_url, download=False)
thumbnails = channel_info["thumbnails"]
best_thumbnail = next(t for t in thumbnails if t["id"] == "avatar_uncropped")
- return best_thumbnail["url"]
+ return str(best_thumbnail["url"])
def get_instagram_avatar_url(channel_name: str) -> str:
@@ -88,10 +88,10 @@ class VideoInfo(TypedDict):
url: str
title: str
description: str
- date_posted: str
+ date_uploaded: str
video_path: Path
thumbnail_path: Path
- subtitle_path: Path
+ subtitle_path: Path | None
channel: ChannelInfo
site: str
@@ -103,7 +103,7 @@ def download_video(url: str) -> VideoInfo:
ydl_opts["outtmpl"] = str(tmp_dir / "%(title)s.%(ext)s")
with YoutubeDL(ydl_opts) as ydl:
- video_info = ydl.extract_info(url)
+ video_info: Any = ydl.extract_info(url)
video_path = next(p for p in tmp_dir.iterdir() if p.suffix == ".mp4")
thumbnail_path = next(p for p in tmp_dir.iterdir() if p.suffix == ".jpg")
@@ -112,6 +112,8 @@ def download_video(url: str) -> VideoInfo:
except StopIteration:
subtitle_path = None
+ channel: ChannelInfo
+
if video_info["extractor"] == "youtube":
site = "youtube"
channel = {
@@ -127,7 +129,7 @@ def download_video(url: str) -> VideoInfo:
channel = {
"id": video_info["uploader_id"],
"name": video_info["uploader"],
- "channel_url": f"https://www.instagram.com/{video_info['channel']}/",
+ "url": f"https://www.instagram.com/{video_info['channel']}/",
"avatar_url": get_instagram_avatar_url(channel_name=video_info["channel"]),
}
else:
@@ -154,7 +156,7 @@ class PathEncoder(json.JSONEncoder):
Custom JSON encoder that encodes paths as a string.
"""
- def default(self, o):
+ def default(self, o: Any) -> Any:
if isinstance(o, Path):
return str(o.absolute())
else: