Update Ruff and fix its errors

This commit is contained in:
Joakim Hellsén 2026-07-21 04:12:13 +02:00
commit 1424978854
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
39 changed files with 183 additions and 175 deletions

View file

@ -21,13 +21,13 @@ repos:
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.30.0 rev: 1.31.1
hooks: hooks:
- id: django-upgrade - id: django-upgrade
args: [--target-version, "6.0"] args: [--target-version, "6.0"]
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17 rev: v0.15.22
hooks: hooks:
- id: ruff-check - id: ruff-check
args: ["--fix", "--exit-non-zero-on-fix"] args: ["--fix", "--exit-non-zero-on-fix"]

View file

@ -144,7 +144,7 @@ class Command(BaseCommand):
Args: Args:
campaign_no (int): The campaign number to import. campaign_no (int): The campaign number to import.
""" """
api_version: str = "v2" # TODO(TheLovinator): Add support for v1 API # noqa: TD003 api_version: str = "v2" # TODO(TheLovinator): Add support for v1 API # ruff:ignore[missing-todo-link]
url: str = f"https://api.chzzk.naver.com/service/{api_version}/drops/campaigns/{campaign_no}" url: str = f"https://api.chzzk.naver.com/service/{api_version}/drops/campaigns/{campaign_no}"
resp: requests.Response = requests.get( resp: requests.Response = requests.get(
url, url,

View file

@ -9,7 +9,7 @@ logger: logging.Logger = logging.getLogger("ttvdrops.tasks")
@shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60) @shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60)
def import_chzzk_campaign_task(self, campaign_no: int) -> None: # noqa: ANN001 def import_chzzk_campaign_task(self, campaign_no: int) -> None: # ruff:ignore[missing-type-function-argument]
"""Import a single Chzzk campaign by its campaign number.""" """Import a single Chzzk campaign by its campaign number."""
try: try:
call_command("import_chzzk_campaign", str(campaign_no)) call_command("import_chzzk_campaign", str(campaign_no))
@ -18,7 +18,7 @@ def import_chzzk_campaign_task(self, campaign_no: int) -> None: # noqa: ANN001
@shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120) @shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120)
def discover_chzzk_campaigns(self) -> None: # noqa: ANN001 def discover_chzzk_campaigns(self) -> None: # ruff:ignore[missing-type-function-argument]
"""Discover and import the latest Chzzk campaigns (equivalent to --latest flag).""" """Discover and import the latest Chzzk campaigns (equivalent to --latest flag)."""
try: try:
call_command("import_chzzk_campaign", latest=True) call_command("import_chzzk_campaign", latest=True)

View file

@ -23,10 +23,10 @@ classes = [
] ]
for cls in classes: for cls in classes:
setattr( # noqa: B010 setattr( # ruff:ignore[set-attr-with-constant]
cls, cls,
"__class_getitem__", "__class_getitem__",
classmethod(lambda cls, *args, **kwargs: cls), # noqa: ARG005 classmethod(lambda cls, *args, **kwargs: cls), # ruff:ignore[unused-lambda-argument]
) )

View file

@ -16,7 +16,7 @@ def celery_app() -> Generator[Celery, Any]:
Yields: Yields:
Celery: A Celery app instance configured for testing. Celery: A Celery app instance configured for testing.
""" """
with patch("os.environ.setdefault") as mock_setenv: # noqa: F841 with patch("os.environ.setdefault") as mock_setenv: # ruff:ignore[unused-variable]
app = Celery("config") app = Celery("config")
app.config_from_object("django.conf:settings", namespace="CELERY") app.config_from_object("django.conf:settings", namespace="CELERY")
yield app yield app

View file

@ -7,7 +7,7 @@ if TYPE_CHECKING:
from collections.abc import Generator from collections.abc import Generator
@pytest.fixture(autouse=True) # noqa: RUF076 — intentional: project-wide N+1 detection with @pytest.mark.no_zeal escape hatch @pytest.fixture(autouse=True)
def use_zeal(request: pytest.FixtureRequest) -> Generator[None]: def use_zeal(request: pytest.FixtureRequest) -> Generator[None]:
"""Enable Zeal N+1 detection context for each pytest test. """Enable Zeal N+1 detection context for each pytest test.

View file

@ -9,7 +9,7 @@ logger: logging.Logger = logging.getLogger("ttvdrops.tasks")
@shared_task(bind=True, queue="default", max_retries=3, default_retry_delay=300) @shared_task(bind=True, queue="default", max_retries=3, default_retry_delay=300)
def submit_indexnow_task(self) -> None: # noqa: ANN001 def submit_indexnow_task(self) -> None: # ruff:ignore[missing-type-function-argument]
"""Submit all site URLs to the IndexNow search index.""" """Submit all site URLs to the IndexNow search index."""
try: try:
call_command("submit_indexnow") call_command("submit_indexnow")

View file

@ -1,4 +1,4 @@
import xml.etree.ElementTree as ET # noqa: S405 import xml.etree.ElementTree as ET # ruff:ignore[suspicious-xml-etree-import]
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from django.urls import reverse from django.urls import reverse
@ -9,7 +9,7 @@ if TYPE_CHECKING:
def _extract_locs(xml_bytes: bytes) -> list[str]: def _extract_locs(xml_bytes: bytes) -> list[str]:
root = ET.fromstring(xml_bytes) # noqa: S314 root = ET.fromstring(xml_bytes) # ruff:ignore[suspicious-xml-element-tree-usage]
ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"} ns = {"s": "http://www.sitemaps.org/schemas/sitemap/0.9"}
return [el.text for el in root.findall(".//s:loc", ns) if el.text] return [el.text for el in root.findall(".//s:loc", ns) if el.text]

View file

@ -1,7 +1,7 @@
from typing import Any from typing import Any
from xml.etree.ElementTree import Element # noqa: S405 from xml.etree.ElementTree import Element # ruff:ignore[suspicious-xml-etree-import]
from xml.etree.ElementTree import SubElement # noqa: S405 from xml.etree.ElementTree import SubElement # ruff:ignore[suspicious-xml-etree-import]
from xml.etree.ElementTree import tostring # noqa: S405 from xml.etree.ElementTree import tostring # ruff:ignore[suspicious-xml-etree-import]
from django.conf import settings from django.conf import settings

View file

@ -53,7 +53,7 @@ MIN_SEARCH_RANK = 0.05
DEFAULT_SITE_DESCRIPTION = "Archive of Twitch drops, campaigns, rewards, and more." DEFAULT_SITE_DESCRIPTION = "Archive of Twitch drops, campaigns, rewards, and more."
def _build_seo_context( # noqa: PLR0913, PLR0917 def _build_seo_context( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
page_title: str = "ttvdrops", page_title: str = "ttvdrops",
page_description: str | None = None, page_description: str | None = None,
page_url: str | None = None, page_url: str | None = None,
@ -91,7 +91,7 @@ def _build_seo_context( # noqa: PLR0913, PLR0917
if page_url and not page_url.startswith("http"): if page_url and not page_url.startswith("http"):
page_url = f"{settings.BASE_URL}{page_url}" page_url = f"{settings.BASE_URL}{page_url}"
# TODO(TheLovinator): Instead of having so many parameters, # noqa: TD003 # TODO(TheLovinator): Instead of having so many parameters, # ruff:ignore[missing-todo-link]
# consider having a single "seo_info" parameter that # consider having a single "seo_info" parameter that
# can contain all of these optional fields. This would make # can contain all of these optional fields. This would make
# it easier to extend in the future without changing the # it easier to extend in the future without changing the
@ -533,7 +533,7 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
# MARK: /debug/ # MARK: /debug/
def debug_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0914 def debug_view(request: HttpRequest) -> HttpResponse: # ruff:ignore[too-many-locals]
"""Debug view showing potentially broken or inconsistent data. """Debug view showing potentially broken or inconsistent data.
Returns: Returns:
@ -772,8 +772,8 @@ def dataset_backups_view(request: HttpRequest) -> HttpResponse:
Returns: Returns:
HttpResponse: The rendered dataset backups page. HttpResponse: The rendered dataset backups page.
""" """
# TODO(TheLovinator): Instead of only using sql we should also support other formats like parquet, csv, or json. # noqa: TD003 # TODO(TheLovinator): Instead of only using sql we should also support other formats like parquet, csv, or json. # ruff:ignore[missing-todo-link]
# TODO(TheLovinator): Upload to s3 instead. # noqa: TD003 # TODO(TheLovinator): Upload to s3 instead. # ruff:ignore[missing-todo-link]
# TODO(TheLovinator): https://developers.google.com/search/docs/appearance/structured-data/dataset#json-ld # TODO(TheLovinator): https://developers.google.com/search/docs/appearance/structured-data/dataset#json-ld
datasets_root: Path = settings.DATA_DIR / "datasets" datasets_root: Path = settings.DATA_DIR / "datasets"
search_dirs: list[Path] = [datasets_root] search_dirs: list[Path] = [datasets_root]
@ -889,7 +889,7 @@ def dataset_backup_download_view(
Raises: Raises:
Http404: When the file is not found or is outside the data directory. Http404: When the file is not found or is outside the data directory.
""" """
# TODO(TheLovinator): Use s3 instead of local disk. # noqa: TD003 # TODO(TheLovinator): Use s3 instead of local disk. # ruff:ignore[missing-todo-link]
datasets_root: Path = settings.DATA_DIR / "datasets" datasets_root: Path = settings.DATA_DIR / "datasets"
requested_path: Path = (datasets_root / relative_path).resolve() requested_path: Path = (datasets_root / relative_path).resolve()
@ -994,7 +994,7 @@ def search_view(request: HttpRequest) -> HttpResponse:
total_results_count: int = sum(len(qs) for qs in results.values()) total_results_count: int = sum(len(qs) for qs in results.values())
# TODO(TheLovinator): Make the description more informative by including counts of each result type, e.g. "Found 5 games, 3 campaigns, and 10 drops for 'rust'." # noqa: TD003 # TODO(TheLovinator): Make the description more informative by including counts of each result type, e.g. "Found 5 games, 3 campaigns, and 10 drops for 'rust'." # ruff:ignore[missing-todo-link]
if query: if query:
page_title: str = f"Search Results for '{query}'"[:60] page_title: str = f"Search Results for '{query}'"[:60]
page_description: str = f"Found {total_results_count} results for '{query}'." page_description: str = f"Found {total_results_count} results for '{query}'."

View file

@ -496,7 +496,7 @@ class KickCategoryCampaignFeed(TTVDropsBaseFeed):
self._limit = None self._limit = None
return super().__call__(request, *args, **kwargs) return super().__call__(request, *args, **kwargs)
def get_object(self, request: HttpRequest, kick_id: int) -> KickCategory: # noqa: ARG002 def get_object(self, request: HttpRequest, kick_id: int) -> KickCategory: # ruff:ignore[unused-method-argument]
"""Return game object for this feed URL.""" """Return game object for this feed URL."""
return KickCategory.objects.get(kick_id=kick_id) return KickCategory.objects.get(kick_id=kick_id)

View file

@ -130,7 +130,7 @@ class Command(BaseCommand):
try: try:
payload: dict = response.json() payload: dict = response.json()
except Exception as exc: # noqa: BLE001 except Exception as exc: # ruff:ignore[blind-except]
self.stderr.write(self.style.ERROR(f"Failed to parse JSON response: {exc}")) self.stderr.write(self.style.ERROR(f"Failed to parse JSON response: {exc}"))
return return
@ -166,7 +166,7 @@ class Command(BaseCommand):
self.style.SUCCESS(f"Imported {imported}/{len(campaigns)} campaign(s)."), self.style.SUCCESS(f"Imported {imported}/{len(campaigns)} campaign(s)."),
) )
def _import_campaign(self, data: KickDropCampaignSchema) -> None: # noqa: PLR0914, PLR0915 def _import_campaign(self, data: KickDropCampaignSchema) -> None: # ruff:ignore[too-many-locals, too-many-statements]
"""Import a single campaign and all its related objects.""" """Import a single campaign and all its related objects."""
# Organization # Organization
org_data: KickOrganizationSchema = data.organization org_data: KickOrganizationSchema = data.organization

View file

@ -93,12 +93,12 @@ class KickCategory(auto_prefetch.Model):
@property @property
def get_absolute_url(self) -> str: def get_absolute_url(self) -> str:
"""Return the URL to the game detail page.""" """The URL to the game detail page."""
return reverse("kick:game_detail", args=[self.kick_id]) return reverse("kick:game_detail", args=[self.kick_id])
@property @property
def kick_url(self) -> str: def kick_url(self) -> str:
"""Return the URL to the game page on Kick.""" """The URL to the game page on Kick."""
return f"https://kick.com/category/{self.slug}" if self.slug else "" return f"https://kick.com/category/{self.slug}" if self.slug else ""
@ -136,7 +136,7 @@ class KickUser(auto_prefetch.Model):
@property @property
def kick_profile_url(self) -> str: def kick_profile_url(self) -> str:
"""Return the Kick profile URL for this user.""" """The Kick profile URL for this user."""
return f"https://kick.com/{self.username}" if self.username else "" return f"https://kick.com/{self.username}" if self.username else ""
@ -183,7 +183,7 @@ class KickChannel(auto_prefetch.Model):
@property @property
def channel_url(self) -> str: def channel_url(self) -> str:
"""Return the Kick channel URL.""" """The Kick channel URL."""
return f"https://kick.com/{self.slug}" if self.slug else "" return f"https://kick.com/{self.slug}" if self.slug else ""
@ -290,7 +290,7 @@ class KickDropCampaign(auto_prefetch.Model):
@property @property
def image_url(self) -> str: def image_url(self) -> str:
"""Return the image URL for the campaign.""" """The image URL for the campaign."""
# Image from first drop # Image from first drop
rewards_prefetched: list[KickReward] | None = getattr( rewards_prefetched: list[KickReward] | None = getattr(
self, self,
@ -356,7 +356,7 @@ class KickDropCampaign(auto_prefetch.Model):
@property @property
def merged_rewards(self) -> list[KickReward]: def merged_rewards(self) -> list[KickReward]:
"""Return rewards de-duplicated by normalized name. """Rewards de-duplicated by normalized name.
If both a base reward and a "(Con)" variant exist, prefer the base reward name. If both a base reward and a "(Con)" variant exist, prefer the base reward name.
""" """
@ -455,7 +455,7 @@ class KickReward(auto_prefetch.Model):
@property @property
def full_image_url(self) -> str: def full_image_url(self) -> str:
"""Return the absolute image URL for this reward. """The absolute image URL for this reward.
If the image_url is a relative path, prepend the Kick image base URL. If the image_url is a relative path, prepend the Kick image base URL.
""" """

View file

@ -1,4 +1,4 @@
from datetime import datetime # noqa: TC003 from datetime import datetime # ruff:ignore[typing-only-standard-library-import]
from pydantic import BaseModel from pydantic import BaseModel
from pydantic import Field from pydantic import Field

View file

@ -9,7 +9,7 @@ logger = logging.getLogger("ttvdrops.tasks")
@shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120) @shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120)
def import_kick_drops(self) -> None: # noqa: ANN001 def import_kick_drops(self) -> None: # ruff:ignore[missing-type-function-argument]
"""Fetch and upsert Kick drop campaigns from the public API.""" """Fetch and upsert Kick drop campaigns from the public API."""
try: try:
call_command("import_kick_drops") call_command("import_kick_drops")

View file

@ -463,7 +463,7 @@ class KickDropCampaignMergedRewardsTest(TestCase):
class ImportKickDropsCommandTest(TestCase): class ImportKickDropsCommandTest(TestCase):
"""Tests for the import_kick_drops management command.""" """Tests for the import_kick_drops management command."""
def _run_command(self, json_payload: dict, **options: Any) -> tuple[str, str]: # noqa: ANN401 def _run_command(self, json_payload: dict, **options: Any) -> tuple[str, str]: # ruff:ignore[any-type]
mock_response = MagicMock() mock_response = MagicMock()
mock_response.json.return_value = json_payload mock_response.json.return_value = json_payload

View file

@ -13,7 +13,7 @@ def main() -> None:
""" """
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
try: try:
from django.core.management import execute_from_command_line # noqa: PLC0415 from django.core.management import execute_from_command_line # ruff:ignore[import-outside-top-level, unsorted-imports]
except ImportError as exc: except ImportError as exc:
msg = ( msg = (
"Couldn't import Django. Are you sure it's installed and " "Couldn't import Django. Are you sure it's installed and "

View file

@ -16,7 +16,7 @@ import argparse
import hashlib import hashlib
import json import json
import shutil import shutil
import subprocess # noqa: S404 import subprocess # ruff:ignore[suspicious-subprocess-import]
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -60,7 +60,7 @@ def run_git(git_dir: str, *args: str, check: bool = True) -> str:
if not git: if not git:
msg = "Git executable not found in PATH." msg = "Git executable not found in PATH."
raise FileNotFoundError(msg) raise FileNotFoundError(msg)
result: subprocess.CompletedProcess[str] = subprocess.run( # noqa: S603 result: subprocess.CompletedProcess[str] = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
[git, "--git-dir", git_dir, *args], [git, "--git-dir", git_dir, *args],
capture_output=True, capture_output=True,
text=True, text=True,

View file

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import datetime # noqa: TC003 import datetime # ruff:ignore[typing-only-standard-library-import]
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Literal from typing import Literal

View file

@ -15,7 +15,7 @@ class TwitchConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField" default_auto_field = "django.db.models.BigAutoField"
name = "twitch" name = "twitch"
def ready(self) -> None: # noqa: D102 def ready(self) -> None: # ruff:ignore[undocumented-public-method]
logger: logging.Logger = logging.getLogger("ttvdrops.apps") logger: logging.Logger = logging.getLogger("ttvdrops.apps")
# Patch FieldFile.open to swallow FileNotFoundError and provide # Patch FieldFile.open to swallow FileNotFoundError and provide
@ -39,18 +39,18 @@ class TwitchConfig(AppConfig):
# Register post_save signal handlers that dispatch image download tasks # Register post_save signal handlers that dispatch image download tasks
# when new Twitch records are created. # when new Twitch records are created.
from django.db.models.signals import m2m_changed # noqa: I001, PLC0415 from django.db.models.signals import m2m_changed # ruff:ignore[unsorted-imports, import-outside-top-level]
from django.db.models.signals import post_save # noqa: PLC0415 from django.db.models.signals import post_save # ruff:ignore[import-outside-top-level]
from twitch.models import DropBenefit # noqa: PLC0415 from twitch.models import DropBenefit # ruff:ignore[import-outside-top-level]
from twitch.models import DropCampaign # noqa: PLC0415 from twitch.models import DropCampaign # ruff:ignore[import-outside-top-level]
from twitch.models import Game # noqa: PLC0415 from twitch.models import Game # ruff:ignore[import-outside-top-level]
from twitch.models import RewardCampaign # noqa: PLC0415 from twitch.models import RewardCampaign # ruff:ignore[import-outside-top-level]
from twitch.signals import on_drop_benefit_saved # noqa: PLC0415 from twitch.signals import on_drop_benefit_saved # ruff:ignore[import-outside-top-level]
from twitch.signals import on_drop_campaign_allow_channels_changed # noqa: PLC0415 from twitch.signals import on_drop_campaign_allow_channels_changed # ruff:ignore[import-outside-top-level]
from twitch.signals import on_drop_campaign_saved # noqa: PLC0415 from twitch.signals import on_drop_campaign_saved # ruff:ignore[import-outside-top-level]
from twitch.signals import on_game_saved # noqa: PLC0415 from twitch.signals import on_game_saved # ruff:ignore[import-outside-top-level]
from twitch.signals import on_reward_campaign_saved # noqa: PLC0415 from twitch.signals import on_reward_campaign_saved # ruff:ignore[import-outside-top-level]
post_save.connect(on_game_saved, sender=Game) post_save.connect(on_game_saved, sender=Game)
post_save.connect(on_drop_campaign_saved, sender=DropCampaign) post_save.connect(on_drop_campaign_saved, sender=DropCampaign)

View file

@ -22,7 +22,7 @@ from django.utils.html import format_html_join
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from core.base_url import build_absolute_uri from core.base_url import build_absolute_uri
from core.base_url import get_current_site # noqa: F811 from core.base_url import get_current_site # ruff:ignore[redefined-while-unused]
from twitch.models import Channel from twitch.models import Channel
from twitch.models import ChatBadge from twitch.models import ChatBadge
from twitch.models import DropCampaign from twitch.models import DropCampaign
@ -185,9 +185,9 @@ class TTVDropsBaseFeed(Feed):
Returns: Returns:
SyndicationFeed: The feed generator instance with the correct site and URL context for absolute URL generation. SyndicationFeed: The feed generator instance with the correct site and URL context for absolute URL generation.
""" """
# TODO(TheLovinator): Refactor to avoid this mess. # noqa: TD003 # TODO(TheLovinator): Refactor to avoid this mess. # ruff:ignore[missing-todo-link]
try: try:
from django.contrib.sites import shortcuts as sites_shortcuts # noqa: I001, PLC0415 from django.contrib.sites import shortcuts as sites_shortcuts # ruff:ignore[unsorted-imports, import-outside-top-level]
except ImportError: except ImportError:
sites_shortcuts = None sites_shortcuts = None
@ -575,7 +575,7 @@ def generate_channels_html(item: Model, *, hide_paid: bool = False) -> list[Safe
) )
if "twitch-chat-badges-guide" in getattr(game, "details_url", ""): if "twitch-chat-badges-guide" in getattr(game, "details_url", ""):
# TODO(TheLovinator): Improve detection of global emotes # noqa: TD003 # TODO(TheLovinator): Improve detection of global emotes # ruff:ignore[missing-todo-link]
parts.append( parts.append(
format_html( format_html(
"{}", "{}",
@ -1177,7 +1177,7 @@ class GameCampaignFeed(TTVDropsBaseFeed):
self._hide_paid = _query_bool(request, "hide_paid") self._hide_paid = _query_bool(request, "hide_paid")
return super().__call__(request, *args, **kwargs) return super().__call__(request, *args, **kwargs)
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002 def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # ruff:ignore[unused-method-argument]
"""Retrieve the Game instance for the given Twitch ID. """Retrieve the Game instance for the given Twitch ID.
Returns: Returns:
@ -1727,7 +1727,7 @@ class GameRewardCampaignFeed(TTVDropsBaseFeed):
self._limit = None self._limit = None
return super().__call__(request, *args, **kwargs) return super().__call__(request, *args, **kwargs)
def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # noqa: ARG002 def get_object(self, request: HttpRequest, twitch_id: str) -> Game: # ruff:ignore[unused-method-argument]
"""Retrieve the Game instance for the given Twitch ID. """Retrieve the Game instance for the given Twitch ID.
Returns: Returns:

View file

@ -14,7 +14,7 @@ class Command(BaseCommand):
help = "Backfill image dimensions for existing cached images" help = "Backfill image dimensions for existing cached images"
def handle(self, *args, **options) -> None: # noqa: ARG002, PLR0915 def handle(self, *args, **options) -> None: # ruff:ignore[unused-method-argument, too-many-statements]
"""Execute the command.""" """Execute the command."""
total_updated = 0 total_updated = 0

View file

@ -2,7 +2,7 @@ import io
import json import json
import os import os
import shutil import shutil
import subprocess # noqa: S404 import subprocess # ruff:ignore[suspicious-subprocess-import]
from compression import zstd from compression import zstd
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
@ -191,11 +191,11 @@ def _write_table_rows(
connection: SQLite connection. connection: SQLite connection.
table: Table name. table: Table name.
""" """
cursor = connection.execute(f'SELECT * FROM "{table}"') # noqa: S608 cursor = connection.execute(f'SELECT * FROM "{table}"') # ruff:ignore[hardcoded-sql-expression]
columns = [description[0] for description in cursor.description] columns = [description[0] for description in cursor.description]
for row in cursor.fetchall(): for row in cursor.fetchall():
values = ", ".join(_sql_literal(row[idx]) for idx in range(len(columns))) values = ", ".join(_sql_literal(row[idx]) for idx in range(len(columns)))
handle.write(f'INSERT INTO "{table}" VALUES ({values});\n') # noqa: S608 handle.write(f'INSERT INTO "{table}" VALUES ({values});\n') # ruff:ignore[hardcoded-sql-expression]
def _write_indexes( def _write_indexes(
@ -268,7 +268,7 @@ def _write_postgres_dump(output_path: Path, tables: list[str]) -> None:
for table in tables: for table in tables:
cmd.extend(["-t", f"public.{table}"]) cmd.extend(["-t", f"public.{table}"])
process = subprocess.Popen( # noqa: S603 process = subprocess.Popen( # ruff:ignore[subprocess-without-shell-equals-true]
cmd, cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
@ -339,7 +339,7 @@ def _write_json_dump(output_path: Path, tables: list[str]) -> None:
data: dict[str, list[dict]] = {} data: dict[str, list[dict]] = {}
with django_connection.cursor() as cursor: with django_connection.cursor() as cursor:
for table in tables: for table in tables:
cursor.execute(f'SELECT * FROM "{table}"') # noqa: S608 cursor.execute(f'SELECT * FROM "{table}"') # ruff:ignore[hardcoded-sql-expression]
columns: list[str] = [col[0] for col in cursor.description] columns: list[str] = [col[0] for col in cursor.description]
rows = cursor.fetchall() rows = cursor.fetchall()
data[table] = [dict(zip(columns, row, strict=False)) for row in rows] data[table] = [dict(zip(columns, row, strict=False)) for row in rows]

View file

@ -403,7 +403,7 @@ def repair_partially_broken_json(raw_text: str) -> str:
wrapped: str = f"[{raw_text}]" wrapped: str = f"[{raw_text}]"
wrapped_data = json.loads(wrapped) wrapped_data = json.loads(wrapped)
# Validate that all items look like GraphQL responses # Validate that all items look like GraphQL responses
if isinstance(wrapped_data, list) and wrapped_data: # noqa: SIM102 if isinstance(wrapped_data, list) and wrapped_data: # ruff:ignore[collapsible-if]
# Check if all items have "data" or "extensions" (GraphQL response structure) # Check if all items have "data" or "extensions" (GraphQL response structure)
if all( if all(
isinstance(item, dict) and ("data" in item or "extensions" in item) isinstance(item, dict) and ("data" in item or "extensions" in item)
@ -452,7 +452,7 @@ def repair_partially_broken_json(raw_text: str) -> str:
valid_lines: list[dict[str, Any]] = [] valid_lines: list[dict[str, Any]] = []
for line in lines: for line in lines:
line: str = line.strip() # noqa: PLW2901 line: str = line.strip() # ruff:ignore[redefined-loop-name]
if line and line.startswith("{"): if line and line.startswith("{"):
try: try:
fixed_line: str = json_repair.repair_json(line, logging=False) fixed_line: str = json_repair.repair_json(line, logging=False)
@ -749,7 +749,7 @@ class Command(BaseCommand):
return channel_obj return channel_obj
def process_responses( # noqa: PLR0915 def process_responses( # ruff:ignore[too-many-statements]
self, self,
responses: list[dict[str, Any]], responses: list[dict[str, Any]],
file_path: Path, file_path: Path,
@ -1180,7 +1180,7 @@ class Command(BaseCommand):
msg: str = f"Path does not exist: {input_path}" msg: str = f"Path does not exist: {input_path}"
raise CommandError(msg) raise CommandError(msg)
def handle(self, *args, **options) -> None: # noqa: ARG002 def handle(self, *args, **options) -> None: # ruff:ignore[unused-method-argument]
"""Main entry point for the command.""" """Main entry point for the command."""
colorama_init(autoreset=True) colorama_init(autoreset=True)

View file

@ -49,7 +49,7 @@ class Command(BaseCommand):
), ),
) )
def handle(self, *args: Any, **options: Any) -> None: # noqa: ANN401, ARG002 def handle(self, *args: Any, **options: Any) -> None: # ruff:ignore[any-type, unused-method-argument]
"""Execute the command to detach the organization and optionally re-import data. """Execute the command to detach the organization and optionally re-import data.
Args: Args:

View file

@ -39,7 +39,7 @@ class Command(BaseCommand):
help="Re-download even if a local box art file already exists.", help="Re-download even if a local box art file already exists.",
) )
def handle( # noqa: PLR0914, PLR0915 def handle( # ruff:ignore[too-many-locals, too-many-statements]
self, self,
*_args: str, *_args: str,
**options: str | bool | int | None, **options: str | bool | int | None,

View file

@ -50,7 +50,7 @@ class Command(BaseCommand):
help="Twitch Access Token (optional - will be obtained automatically if not provided)", help="Twitch Access Token (optional - will be obtained automatically if not provided)",
) )
def handle(self, *args, **options) -> None: # noqa: ARG002 def handle(self, *args, **options) -> None: # ruff:ignore[unused-method-argument]
"""Main entry point for the command. """Main entry point for the command.
Raises: Raises:

View file

@ -27,7 +27,7 @@ import hashlib
import json import json
import os import os
import pathlib import pathlib
import subprocess # noqa: S404 import subprocess # ruff:ignore[suspicious-subprocess-import]
import tempfile import tempfile
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Any from typing import Any
@ -194,7 +194,7 @@ class Command(BaseCommand):
), ),
) )
def handle(self, *args: Any, **options: Any) -> None: # noqa: ANN401, ARG002, PLR0914, PLR0915 def handle(self, *args: Any, **options: Any) -> None: # ruff:ignore[any-type, unused-method-argument, too-many-locals, too-many-statements]
"""Main entry point for the command. """Main entry point for the command.
Raises: Raises:
@ -301,7 +301,7 @@ class Command(BaseCommand):
crash_on_error=crash_on_error, crash_on_error=crash_on_error,
skip_existing=skip_existing, skip_existing=skip_existing,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # ruff:ignore[blind-except]
msg = f"Failed to import drops.json: {exc}" msg = f"Failed to import drops.json: {exc}"
self.stderr.write(self.style.ERROR(msg)) self.stderr.write(self.style.ERROR(msg))
errors.append(msg) errors.append(msg)
@ -315,7 +315,7 @@ class Command(BaseCommand):
crash_on_error=crash_on_error, crash_on_error=crash_on_error,
skip_existing=skip_existing, skip_existing=skip_existing,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # ruff:ignore[blind-except]
msg = f"Failed to import rewards.json: {exc}" msg = f"Failed to import rewards.json: {exc}"
self.stderr.write(self.style.ERROR(msg)) self.stderr.write(self.style.ERROR(msg))
errors.append(msg) errors.append(msg)
@ -353,7 +353,7 @@ class Command(BaseCommand):
# Historical import via git clone # Historical import via git clone
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _process_historical( # noqa: PLR0913 def _process_historical( # ruff:ignore[too-many-arguments]
self, self,
source: str, source: str,
*, *,
@ -423,7 +423,7 @@ class Command(BaseCommand):
max_commits=max_commits, max_commits=max_commits,
skip_existing=skip_existing, skip_existing=skip_existing,
) )
except Exception as exc: # noqa: BLE001 except Exception as exc: # ruff:ignore[blind-except]
errors.append(str(exc)) errors.append(str(exc))
finally: finally:
if own_clone: if own_clone:
@ -431,7 +431,7 @@ class Command(BaseCommand):
return drops_count, rewards_count, errors return drops_count, rewards_count, errors
def _import_historical_file( # noqa: PLR0913 def _import_historical_file( # ruff:ignore[too-many-arguments]
self, self,
git_dir: str, git_dir: str,
file_path: str, file_path: str,
@ -581,7 +581,7 @@ class Command(BaseCommand):
Stdout of the git command. Stdout of the git command.
""" """
cmd = ["git", "--git-dir", git_dir, *args] cmd = ["git", "--git-dir", git_dir, *args]
result = subprocess.run(cmd, capture_output=True, text=True, check=check) # noqa: S603 result = subprocess.run(cmd, capture_output=True, text=True, check=check) # ruff:ignore[subprocess-without-shell-equals-true]
return result.stdout return result.stdout
def _rmtree(self, path: str) -> None: def _rmtree(self, path: str) -> None:
@ -659,7 +659,7 @@ class Command(BaseCommand):
) )
@staticmethod @staticmethod
def _strip_typename(data: Any) -> Any: # noqa: ANN401 def _strip_typename(data: Any) -> Any: # ruff:ignore[any-type]
"""Recursively remove ``__typename`` keys from parsed JSON data. """Recursively remove ``__typename`` keys from parsed JSON data.
Old commits in the repo included GraphQL ``__typename`` metadata which Old commits in the repo included GraphQL ``__typename`` metadata which
@ -683,7 +683,7 @@ class Command(BaseCommand):
return [Command._strip_typename(item) for item in data] return [Command._strip_typename(item) for item in data]
return data return data
def _generate_report( # noqa: PLR0914, PLR0915 def _generate_report( # ruff:ignore[too-many-locals, too-many-statements]
self, self,
drops_url: str, drops_url: str,
rewards_url: str, rewards_url: str,
@ -707,7 +707,7 @@ class Command(BaseCommand):
Raises: Raises:
ValidationError: If any item fails schema validation and crash_on_error is True. ValidationError: If any item fails schema validation and crash_on_error is True.
""" """
from collections import Counter # noqa: PLC0415 from collections import Counter # ruff:ignore[import-outside-top-level]
self.stdout.write("=" * 60) self.stdout.write("=" * 60)
self.stdout.write(self.style.SUCCESS("COMPARISON REPORT")) self.stdout.write(self.style.SUCCESS("COMPARISON REPORT"))
@ -719,14 +719,14 @@ class Command(BaseCommand):
# Track differences for field-level comparison # Track differences for field-level comparison
sample_limit = 5 sample_limit = 5
for label, raw_url, model_cls, id_field in [ # noqa: PLR1702 for label, raw_url, model_cls, id_field in [ # ruff:ignore[too-many-nested-blocks]
("drops", drops_url, DropCampaign, "twitch_id"), ("drops", drops_url, DropCampaign, "twitch_id"),
("rewards", rewards_url, RewardCampaign, "twitch_id"), ("rewards", rewards_url, RewardCampaign, "twitch_id"),
]: ]:
self.stdout.write(f"\n--- {label.upper()} ---") self.stdout.write(f"\n--- {label.upper()} ---")
try: try:
raw_data = self._fetch_json(raw_url, f"{label}.json") raw_data = self._fetch_json(raw_url, f"{label}.json")
except Exception as exc: # noqa: BLE001 except Exception as exc: # ruff:ignore[blind-except]
self.stderr.write( self.stderr.write(
self.style.ERROR(f" Failed to fetch {label}.json: {exc}"), self.style.ERROR(f" Failed to fetch {label}.json: {exc}"),
) )
@ -894,7 +894,7 @@ class Command(BaseCommand):
self.stdout.write("\n" + "=" * 60) self.stdout.write("\n" + "=" * 60)
def _parse_and_import_drops( # noqa: PLR0913 def _parse_and_import_drops( # ruff:ignore[too-many-arguments]
self, self,
raw_data: list[dict[str, Any]], raw_data: list[dict[str, Any]],
source: str, source: str,
@ -976,7 +976,7 @@ class Command(BaseCommand):
return total_campaigns return total_campaigns
def _process_sunkwibot_reward( # noqa: PLR0915 def _process_sunkwibot_reward( # ruff:ignore[too-many-statements]
self, self,
reward: SunkwiBotRewardSchema, reward: SunkwiBotRewardSchema,
game_obj: Game, game_obj: Game,
@ -1133,7 +1133,7 @@ class Command(BaseCommand):
self, self,
tbd_schema: SunkwiBotTimeBasedDropSchema, tbd_schema: SunkwiBotTimeBasedDropSchema,
campaign_obj: DropCampaign, campaign_obj: DropCampaign,
source: str, # noqa: ARG002 source: str, # ruff:ignore[unused-method-argument]
*, *,
verbose: bool, verbose: bool,
) -> None: ) -> None:
@ -1335,7 +1335,7 @@ class Command(BaseCommand):
skip_existing=skip_existing, skip_existing=skip_existing,
) )
def _parse_and_import_rewards( # noqa: PLR0913 def _parse_and_import_rewards( # ruff:ignore[too-many-arguments]
self, self,
raw_data: list[dict[str, Any]], raw_data: list[dict[str, Any]],
source: str, source: str,
@ -1563,12 +1563,12 @@ class Command(BaseCommand):
self._org_cache[twitch_id] = org_obj self._org_cache[twitch_id] = org_obj
return org_obj return org_obj
def _get_or_create_game( # noqa: PLR0913 def _get_or_create_game( # ruff:ignore[too-many-arguments]
self, self,
twitch_id: str, twitch_id: str,
display_name: str, display_name: str,
box_art_url: str, box_art_url: str,
source: str, # noqa: ARG002 source: str, # ruff:ignore[unused-method-argument]
*, *,
verbose: bool, verbose: bool,
org_obj: Organization | None = None, org_obj: Organization | None = None,

View file

@ -37,7 +37,7 @@ class Command(BaseCommand):
help="Path to directory to watch for JSON files", help="Path to directory to watch for JSON files",
) )
def handle(self, *args, **options) -> None: # noqa: ARG002 def handle(self, *args, **options) -> None: # ruff:ignore[unused-method-argument]
"""Main entry point for the watch command. """Main entry point for the watch command.
Args: Args:
@ -87,7 +87,7 @@ class Command(BaseCommand):
importer_command: An instance of the BetterImportDropsCommand to handle the import logic. importer_command: An instance of the BetterImportDropsCommand to handle the import logic.
watch_path: The directory path to watch for JSON files. watch_path: The directory path to watch for JSON files.
""" """
# TODO(TheLovinator): Implement actual file watching using watchdog or similar library. # noqa: TD003 # TODO(TheLovinator): Implement actual file watching using watchdog or similar library. # ruff:ignore[missing-todo-link]
json_files: list[Path] = [ json_files: list[Path] = [
f for f in watch_path.iterdir() if f.suffix == ".json" and f.is_file() f for f in watch_path.iterdir() if f.suffix == ".json" and f.is_file()
] ]

View file

@ -5,7 +5,7 @@ from django.db import migrations
from django.db import models from django.db import models
def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001 def migrate_operation_name_to_list(apps, schema_editor) -> None: # ruff:ignore[missing-type-function-argument]
"""Convert operation_name string values to operation_names list.""" """Convert operation_name string values to operation_names list."""
DropCampaign = apps.get_model("twitch", "DropCampaign") DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all(): for campaign in DropCampaign.objects.all():
@ -14,7 +14,7 @@ def migrate_operation_name_to_list(apps, schema_editor) -> None: # noqa: ANN001
campaign.save(update_fields=["operation_names"]) campaign.save(update_fields=["operation_names"])
def reverse_operation_names_to_string(apps, schema_editor) -> None: # noqa: ANN001 def reverse_operation_names_to_string(apps, schema_editor) -> None: # ruff:ignore[missing-type-function-argument]
"""Convert operation_names list back to operation_name string (first item only).""" """Convert operation_names list back to operation_name string (first item only)."""
DropCampaign = apps.get_model("twitch", "DropCampaign") DropCampaign = apps.get_model("twitch", "DropCampaign")
for campaign in DropCampaign.objects.all(): for campaign in DropCampaign.objects.all():

View file

@ -1,7 +1,7 @@
from django.db import migrations from django.db import migrations
def mark_all_drops_fully_imported(apps, schema_editor) -> None: # noqa: ANN001 def mark_all_drops_fully_imported(apps, schema_editor) -> None: # ruff:ignore[missing-type-function-argument]
"""Marks all existing DropCampaigns as fully imported. """Marks all existing DropCampaigns as fully imported.
This was needed to ensure that the Twitch API view only returns campaigns that are ready for display. This was needed to ensure that the Twitch API view only returns campaigns that are ready for display.

View file

@ -137,7 +137,7 @@ class Game(auto_prefetch.Model):
default="", default="",
) )
box_art = models.URLField( # noqa: DJ001 box_art = models.URLField( # ruff:ignore[django-nullable-model-string-field]
verbose_name="Box art URL", verbose_name="Box art URL",
max_length=500, max_length=500,
blank=True, blank=True,
@ -231,12 +231,12 @@ class Game(auto_prefetch.Model):
@property @property
def organizations(self) -> models.QuerySet[Organization]: def organizations(self) -> models.QuerySet[Organization]:
"""Return orgs that own games with campaigns for this game.""" """Orgs that own games with campaigns for this game."""
return Organization.objects.filter(games__drop_campaigns__game=self).distinct() return Organization.objects.filter(games__drop_campaigns__game=self).distinct()
@property @property
def get_game_name(self) -> str: def get_game_name(self) -> str:
"""Return the best available name for the game.""" """The best available name for the game."""
if self.display_name: if self.display_name:
return self.display_name return self.display_name
if self.name: if self.name:
@ -247,8 +247,8 @@ class Game(auto_prefetch.Model):
@property @property
def twitch_directory_url(self) -> str: def twitch_directory_url(self) -> str:
"""Return Twitch directory URL with drops filter when slug exists.""" """Twitch directory URL with drops filter when slug exists."""
# TODO(TheLovinator): If no slug, get from Twitch API or IGDB? # noqa: TD003 # TODO(TheLovinator): If no slug, get from Twitch API or IGDB? # ruff:ignore[missing-todo-link]
if self.slug: if self.slug:
return f"https://www.twitch.tv/directory/category/{self.slug}?filter=drops" return f"https://www.twitch.tv/directory/category/{self.slug}?filter=drops"
@ -256,7 +256,7 @@ class Game(auto_prefetch.Model):
@property @property
def box_art_best_url(self) -> str: def box_art_best_url(self) -> str:
"""Return the best available URL for the game's box art (local first).""" """The best available URL for the game's box art (local first)."""
try: try:
if self.box_art_file and getattr(self.box_art_file, "url", None): if self.box_art_file and getattr(self.box_art_file, "url", None):
return self.box_art_file.url return self.box_art_file.url
@ -271,7 +271,7 @@ class Game(auto_prefetch.Model):
@property @property
def dashboard_box_art_url(self) -> str: def dashboard_box_art_url(self) -> str:
"""Return dashboard-safe box art URL without touching deferred image fields.""" """Dashboard-safe box art URL without touching deferred image fields."""
return normalize_twitch_box_art_url(self.box_art or "") return normalize_twitch_box_art_url(self.box_art or "")
@classmethod @classmethod
@ -570,7 +570,7 @@ class Channel(auto_prefetch.Model):
@property @property
def preferred_name(self) -> str: def preferred_name(self) -> str:
"""Return display name fallback used by channel-facing pages.""" """Display name fallback used by channel-facing pages."""
return self.display_name or self.name or self.twitch_id return self.display_name or self.name or self.twitch_id
def detail_description(self, total_campaigns: int) -> str: def detail_description(self, total_campaigns: int) -> str:
@ -580,7 +580,7 @@ class Channel(auto_prefetch.Model):
# MARK: DropCampaign # MARK: DropCampaign
class DropCampaign(auto_prefetch.Model): # noqa: PLR0904 class DropCampaign(auto_prefetch.Model): # ruff:ignore[too-many-public-methods]
"""Represents a Twitch drop campaign.""" """Represents a Twitch drop campaign."""
twitch_id = models.TextField( twitch_id = models.TextField(
@ -1270,7 +1270,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def clean_name(self) -> str: def clean_name(self) -> str:
"""Return the campaign name without the game name prefix. """The campaign name without the game name prefix.
Examples: Examples:
"Ravendawn - July 2" -> "July 2" "Ravendawn - July 2" -> "July 2"
@ -1302,7 +1302,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def single_reward_benefit(self) -> DropBenefit | None: def single_reward_benefit(self) -> DropBenefit | None:
"""Return the only unique reward benefit for this campaign, if it has one.""" """The only unique reward benefit for this campaign, if it has one."""
benefits: list[DropBenefit] = [] benefits: list[DropBenefit] = []
seen_benefit_keys: set[int | str] = set() seen_benefit_keys: set[int | str] = set()
@ -1321,7 +1321,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def single_reward_image_best_url(self) -> str: def single_reward_image_best_url(self) -> str:
"""Return the best image URL for a campaign that has exactly one reward.""" """The best image URL for a campaign that has exactly one reward."""
benefit: DropBenefit | None = self.single_reward_benefit benefit: DropBenefit | None = self.single_reward_benefit
if not benefit: if not benefit:
return "" return ""
@ -1329,12 +1329,12 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def meta_image_url(self) -> str: def meta_image_url(self) -> str:
"""Return the preferred campaign image URL for SEO metadata.""" """The preferred campaign image URL for SEO metadata."""
return self.single_reward_image_best_url or self.image_best_url return self.single_reward_image_best_url or self.image_best_url
@property @property
def image_best_url(self) -> str: def image_best_url(self) -> str:
"""Return the best URL for the campaign image. """The best URL for the campaign image.
Priority: Priority:
1. Local cached image file 1. Local cached image file
@ -1361,7 +1361,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def listing_image_url(self) -> str: def listing_image_url(self) -> str:
"""Return a campaign image URL optimized for list views. """A campaign image URL optimized for list views.
This intentionally avoids traversing drops/benefits to prevent N+1 queries This intentionally avoids traversing drops/benefits to prevent N+1 queries
in list pages that render many campaigns. in list pages that render many campaigns.
@ -1375,7 +1375,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def dashboard_image_url(self) -> str: def dashboard_image_url(self) -> str:
"""Return dashboard-safe campaign or single-reward image URL.""" """Dashboard-safe campaign or single-reward image URL."""
benefit: DropBenefit | None = self.single_reward_benefit benefit: DropBenefit | None = self.single_reward_benefit
if benefit and benefit.image_asset_url: if benefit and benefit.image_asset_url:
return benefit.image_asset_url return benefit.image_asset_url
@ -1383,7 +1383,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def duration_iso(self) -> str: def duration_iso(self) -> str:
"""Return the campaign duration in ISO 8601 format (e.g., 'P3DT4H30M'). """The campaign duration in ISO 8601 format (e.g., 'P3DT4H30M').
This is used for the <time> element's datetime attribute to provide This is used for the <time> element's datetime attribute to provide
machine-readable duration. If start_at or end_at is missing, returns machine-readable duration. If start_at or end_at is missing, returns
@ -1421,7 +1421,7 @@ class DropCampaign(auto_prefetch.Model): # noqa: PLR0904
@property @property
def sorted_benefits(self) -> list[DropBenefit]: def sorted_benefits(self) -> list[DropBenefit]:
"""Return a sorted list of benefits for the campaign.""" """A sorted list of benefits for the campaign."""
benefits: list[DropBenefit] = [] benefits: list[DropBenefit] = []
for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue] for drop in self.time_based_drops.all(): # pyright: ignore[reportAttributeAccessIssue]
benefits.extend(drop.benefits.all()) # pyright: ignore[reportAttributeAccessIssue] benefits.extend(drop.benefits.all()) # pyright: ignore[reportAttributeAccessIssue]
@ -1570,7 +1570,7 @@ class DropBenefit(auto_prefetch.Model):
@property @property
def image_best_url(self) -> str: def image_best_url(self) -> str:
"""Return the best URL for the benefit image (local first).""" """The best URL for the benefit image (local first)."""
try: try:
if self.image_file: if self.image_file:
file_name: str = getattr(self.image_file, "name", "") file_name: str = getattr(self.image_file, "name", "")
@ -1917,7 +1917,7 @@ class RewardCampaign(auto_prefetch.Model):
@property @property
def image_best_url(self) -> str: def image_best_url(self) -> str:
"""Return the best URL for the reward campaign image (local first).""" """The best URL for the reward campaign image (local first)."""
try: try:
if self.image_file and getattr(self.image_file, "url", None): if self.image_file and getattr(self.image_file, "url", None):
return self.image_file.url return self.image_file.url
@ -2022,14 +2022,14 @@ class ChatBadge(auto_prefetch.Model):
verbose_name="Description", verbose_name="Description",
) )
click_action = models.TextField( # noqa: DJ001 click_action = models.TextField( # ruff:ignore[django-nullable-model-string-field]
help_text="The action to take when clicking on the badge (e.g., 'visit_url').", help_text="The action to take when clicking on the badge (e.g., 'visit_url').",
verbose_name="Click Action", verbose_name="Click Action",
blank=True, blank=True,
null=True, null=True,
) )
click_url = models.URLField( # noqa: DJ001 click_url = models.URLField( # ruff:ignore[django-nullable-model-string-field]
help_text="The URL to navigate to when clicking on the badge.", help_text="The URL to navigate to when clicking on the badge.",
verbose_name="Click URL", verbose_name="Click URL",
max_length=500, max_length=500,

View file

@ -8,11 +8,11 @@ from django.db.models import Count
logger = logging.getLogger("ttvdrops.signals") logger = logging.getLogger("ttvdrops.signals")
def _dispatch(task_fn: Any, pk: int) -> None: # noqa: ANN401 def _dispatch(task_fn: Any, pk: int) -> None: # ruff:ignore[any-type]
"""Dispatch a Celery task, logging rather than raising when the broker is unavailable.""" """Dispatch a Celery task, logging rather than raising when the broker is unavailable."""
try: try:
task_fn.delay(pk) task_fn.delay(pk)
except Exception: # noqa: BLE001 except Exception: # ruff:ignore[blind-except]
logger.debug( logger.debug(
"Could not dispatch %s(%d) — broker may be unavailable.", "Could not dispatch %s(%d) — broker may be unavailable.",
task_fn.name, task_fn.name,
@ -20,49 +20,57 @@ def _dispatch(task_fn: Any, pk: int) -> None: # noqa: ANN401
) )
def on_game_saved(sender: Any, instance: Any, created: bool, **kwargs: Any) -> None: # noqa: ANN401, FBT001 def on_game_saved(sender: Any, instance: Any, created: bool, **kwargs: Any) -> None: # ruff:ignore[any-type, boolean-type-hint-positional-argument]
"""Dispatch a box-art download task when a new Game is created.""" """Dispatch a box-art download task when a new Game is created."""
if created: if created:
from twitch.tasks import download_game_image # noqa: PLC0415 from twitch.tasks import ( # ruff:ignore[import-outside-top-level]
download_game_image,
)
_dispatch(download_game_image, instance.pk) _dispatch(download_game_image, instance.pk)
def on_drop_campaign_saved( def on_drop_campaign_saved(
sender: Any, # noqa: ANN401 sender: Any, # ruff:ignore[any-type]
instance: Any, # noqa: ANN401 instance: Any, # ruff:ignore[any-type]
created: bool, # noqa: FBT001 created: bool, # ruff:ignore[boolean-type-hint-positional-argument]
**kwargs: Any, # noqa: ANN401 **kwargs: Any, # ruff:ignore[any-type]
) -> None: ) -> None:
"""Dispatch an image download task when a new DropCampaign is created.""" """Dispatch an image download task when a new DropCampaign is created."""
if created: if created:
from twitch.tasks import download_campaign_image # noqa: PLC0415 from twitch.tasks import ( # ruff:ignore[import-outside-top-level]
download_campaign_image,
)
_dispatch(download_campaign_image, instance.pk) _dispatch(download_campaign_image, instance.pk)
def on_drop_benefit_saved( def on_drop_benefit_saved(
sender: Any, # noqa: ANN401 sender: Any, # ruff:ignore[any-type]
instance: Any, # noqa: ANN401 instance: Any, # ruff:ignore[any-type]
created: bool, # noqa: FBT001 created: bool, # ruff:ignore[boolean-type-hint-positional-argument]
**kwargs: Any, # noqa: ANN401 **kwargs: Any, # ruff:ignore[any-type]
) -> None: ) -> None:
"""Dispatch an image download task when a new DropBenefit is created.""" """Dispatch an image download task when a new DropBenefit is created."""
if created: if created:
from twitch.tasks import download_benefit_image # noqa: PLC0415 from twitch.tasks import ( # ruff:ignore[import-outside-top-level]
download_benefit_image,
)
_dispatch(download_benefit_image, instance.pk) _dispatch(download_benefit_image, instance.pk)
def on_reward_campaign_saved( def on_reward_campaign_saved(
sender: Any, # noqa: ANN401 sender: Any, # ruff:ignore[any-type]
instance: Any, # noqa: ANN401 instance: Any, # ruff:ignore[any-type]
created: bool, # noqa: FBT001 created: bool, # ruff:ignore[boolean-type-hint-positional-argument]
**kwargs: Any, # noqa: ANN401 **kwargs: Any, # ruff:ignore[any-type]
) -> None: ) -> None:
"""Dispatch an image download task when a new RewardCampaign is created.""" """Dispatch an image download task when a new RewardCampaign is created."""
if created: if created:
from twitch.tasks import download_reward_campaign_image # noqa: PLC0415 from twitch.tasks import ( # ruff:ignore[import-outside-top-level]
download_reward_campaign_image,
)
_dispatch(download_reward_campaign_image, instance.pk) _dispatch(download_reward_campaign_image, instance.pk)
@ -72,8 +80,8 @@ def _refresh_allowed_campaign_counts(channel_ids: set[int]) -> None:
if not channel_ids: if not channel_ids:
return return
from twitch.models import Channel # noqa: PLC0415 from twitch.models import Channel # ruff:ignore[import-outside-top-level]
from twitch.models import DropCampaign # noqa: PLC0415 from twitch.models import DropCampaign # ruff:ignore[import-outside-top-level]
through_model: type[Channel] = DropCampaign.allow_channels.through through_model: type[Channel] = DropCampaign.allow_channels.through
counts_by_channel: dict[int, int] = { counts_by_channel: dict[int, int] = {
@ -96,19 +104,19 @@ def _refresh_allowed_campaign_counts(channel_ids: set[int]) -> None:
Channel.objects.bulk_update(channels, ["allowed_campaign_count"]) Channel.objects.bulk_update(channels, ["allowed_campaign_count"])
def on_drop_campaign_allow_channels_changed( # noqa: PLR0913, PLR0917 def on_drop_campaign_allow_channels_changed( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
sender: Any, # noqa: ANN401 sender: Any, # ruff:ignore[any-type]
instance: Any, # noqa: ANN401 instance: Any, # ruff:ignore[any-type]
action: str, action: str,
reverse: bool, # noqa: FBT001 reverse: bool, # ruff:ignore[boolean-type-hint-positional-argument]
model: Any, # noqa: ANN401 model: Any, # ruff:ignore[any-type]
pk_set: set[int] | None, pk_set: set[int] | None,
**kwargs: Any, # noqa: ANN401 **kwargs: Any, # ruff:ignore[any-type]
) -> None: ) -> None:
"""Keep Channel.allowed_campaign_count in sync for allow_channels M2M changes.""" """Keep Channel.allowed_campaign_count in sync for allow_channels M2M changes."""
if action == "pre_clear" and not reverse: if action == "pre_clear" and not reverse:
# post_clear does not expose removed channel IDs; snapshot before clearing. # post_clear does not expose removed channel IDs; snapshot before clearing.
instance._pre_clear_channel_ids = set( # pyright: ignore[reportAttributeAccessIssue] # noqa: SLF001 instance._pre_clear_channel_ids = set( # pyright: ignore[reportAttributeAccessIssue] # ruff:ignore[private-member-access]
instance.allow_channels.values_list("pk", flat=True), # pyright: ignore[reportAttributeAccessIssue] instance.allow_channels.values_list("pk", flat=True), # pyright: ignore[reportAttributeAccessIssue]
) )
return return

View file

@ -21,7 +21,7 @@ logger: logging.Logger = logging.getLogger("ttvdrops.tasks")
@shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60) @shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60)
def scan_pending_twitch_files(self) -> None: # noqa: ANN001 def scan_pending_twitch_files(self) -> None: # ruff:ignore[missing-type-function-argument]
"""Scan TTVDROPS_PENDING_DIR for JSON files and dispatch an import task for each.""" """Scan TTVDROPS_PENDING_DIR for JSON files and dispatch an import task for each."""
pending_dir: str = os.getenv("TTVDROPS_PENDING_DIR", "") pending_dir: str = os.getenv("TTVDROPS_PENDING_DIR", "")
if not pending_dir: if not pending_dir:
@ -44,9 +44,9 @@ def scan_pending_twitch_files(self) -> None: # noqa: ANN001
@shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60) @shared_task(bind=True, queue="imports", max_retries=3, default_retry_delay=60)
def import_twitch_file(self, file_path: str) -> None: # noqa: ANN001 def import_twitch_file(self, file_path: str) -> None: # ruff:ignore[missing-type-function-argument]
"""Import a single Twitch JSON drop file via BetterImportDrops logic.""" """Import a single Twitch JSON drop file via BetterImportDrops logic."""
from twitch.management.commands.better_import_drops import Command as Importer # noqa: I001, PLC0415 from twitch.management.commands.better_import_drops import Command as Importer # ruff:ignore[unsorted-imports, import-outside-top-level]
path = Path(file_path) path = Path(file_path)
if not path.is_file(): if not path.is_file():
@ -103,7 +103,7 @@ def _convert_to_modern_formats(source: Path) -> None:
try: try:
img = _open_and_prepare_image(source) img = _open_and_prepare_image(source)
except Exception: # noqa: BLE001 except Exception: # ruff:ignore[blind-except]
logger.debug("Format conversion failed for %s.", source) logger.debug("Format conversion failed for %s.", source)
return return
@ -116,7 +116,7 @@ def _open_and_prepare_image(source: Path) -> Image:
Returns: Returns:
An RGB-mode PIL Image ready for saving in modern formats. An RGB-mode PIL Image ready for saving in modern formats.
""" """
from PIL import Image # noqa: PLC0415 from PIL import Image # ruff:ignore[import-outside-top-level]
with Image.open(source) as raw: with Image.open(source) as raw:
if raw.mode in {"RGBA", "LA"} or ( if raw.mode in {"RGBA", "LA"} or (
@ -140,7 +140,7 @@ def _save_modern_formats(img: Image, source: Path) -> None:
out: Path = source.with_suffix(ext) out: Path = source.with_suffix(ext)
try: try:
img.save(out, fmt, quality=80) img.save(out, fmt, quality=80)
except Exception: # noqa: BLE001 except Exception: # ruff:ignore[blind-except]
logger.debug("Could not convert %s to %s.", source, fmt) logger.debug("Could not convert %s to %s.", source, fmt)
@ -150,11 +150,11 @@ def _save_modern_formats(img: Image, source: Path) -> None:
@shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300) @shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300)
def download_game_image(self, game_pk: int) -> None: # noqa: ANN001 def download_game_image(self, game_pk: int) -> None: # ruff:ignore[missing-type-function-argument]
"""Download and cache the box art image for a single Game.""" """Download and cache the box art image for a single Game."""
from twitch.models import Game # noqa: PLC0415 from twitch.models import Game # ruff:ignore[import-outside-top-level, unsorted-imports]
from twitch.utils import is_twitch_box_art_url # noqa: PLC0415 from twitch.utils import is_twitch_box_art_url # ruff:ignore[import-outside-top-level]
from twitch.utils import normalize_twitch_box_art_url # noqa: PLC0415 from twitch.utils import normalize_twitch_box_art_url # ruff:ignore[import-outside-top-level]
try: try:
game = Game.objects.get(pk=game_pk) game = Game.objects.get(pk=game_pk)
@ -172,9 +172,9 @@ def download_game_image(self, game_pk: int) -> None: # noqa: ANN001
@shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300) @shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300)
def download_campaign_image(self, campaign_pk: int) -> None: # noqa: ANN001 def download_campaign_image(self, campaign_pk: int) -> None: # ruff:ignore[missing-type-function-argument]
"""Download and cache the image for a single DropCampaign.""" """Download and cache the image for a single DropCampaign."""
from twitch.models import DropCampaign # noqa: PLC0415 from twitch.models import DropCampaign # ruff:ignore[import-outside-top-level]
try: try:
campaign = DropCampaign.objects.get(pk=campaign_pk) campaign = DropCampaign.objects.get(pk=campaign_pk)
@ -191,9 +191,9 @@ def download_campaign_image(self, campaign_pk: int) -> None: # noqa: ANN001
@shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300) @shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300)
def download_benefit_image(self, benefit_pk: int) -> None: # noqa: ANN001 def download_benefit_image(self, benefit_pk: int) -> None: # ruff:ignore[missing-type-function-argument]
"""Download and cache the image for a single DropBenefit.""" """Download and cache the image for a single DropBenefit."""
from twitch.models import DropBenefit # noqa: PLC0415 from twitch.models import DropBenefit # ruff:ignore[import-outside-top-level]
try: try:
benefit = DropBenefit.objects.get(pk=benefit_pk) benefit = DropBenefit.objects.get(pk=benefit_pk)
@ -214,9 +214,9 @@ def download_benefit_image(self, benefit_pk: int) -> None: # noqa: ANN001
@shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300) @shared_task(bind=True, queue="image-downloads", max_retries=3, default_retry_delay=300)
def download_reward_campaign_image(self, reward_pk: int) -> None: # noqa: ANN001 def download_reward_campaign_image(self, reward_pk: int) -> None: # ruff:ignore[missing-type-function-argument]
"""Download and cache the image for a single RewardCampaign.""" """Download and cache the image for a single RewardCampaign."""
from twitch.models import RewardCampaign # noqa: PLC0415 from twitch.models import RewardCampaign # ruff:ignore[import-outside-top-level]
try: try:
reward = RewardCampaign.objects.get(pk=reward_pk) reward = RewardCampaign.objects.get(pk=reward_pk)
@ -245,7 +245,7 @@ def download_all_images() -> None:
@shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120) @shared_task(bind=True, queue="api-fetches", max_retries=3, default_retry_delay=120)
def import_chat_badges(self) -> None: # noqa: ANN001 def import_chat_badges(self) -> None: # ruff:ignore[missing-type-function-argument]
"""Fetch and upsert Twitch global chat badges via the Helix API.""" """Fetch and upsert Twitch global chat badges via the Helix API."""
try: try:
call_command("import_chat_badges") call_command("import_chat_badges")

View file

@ -46,7 +46,7 @@ def get_format_url(image_url: str, fmt: str) -> str:
@register.simple_tag @register.simple_tag
def picture( # noqa: PLR0913, PLR0917 def picture( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
src: str, src: str,
alt: str = "", alt: str = "",
width: int | None = None, width: int | None = None,

View file

@ -1080,7 +1080,7 @@ def test_campaign_feed_queries_bounded(
_build_campaign(game, i) _build_campaign(game, i)
url: str = reverse("core:campaign_feed") url: str = reverse("core:campaign_feed")
# TODO(TheLovinator): 14 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003 # TODO(TheLovinator): 14 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # ruff:ignore[missing-todo-link]
with django_assert_num_queries(14, exact=False): with django_assert_num_queries(14, exact=False):
response: _MonkeyPatchedWSGIResponse = client.get(url) response: _MonkeyPatchedWSGIResponse = client.get(url)
@ -1331,7 +1331,7 @@ def test_docs_rss_queries_bounded(
url: str = reverse("core:docs_rss") url: str = reverse("core:docs_rss")
# TODO(TheLovinator): 31 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # noqa: TD003 # TODO(TheLovinator): 31 queries is still quite high for a feed - we should be able to optimize this further, but this is a good starting point to prevent regressions for now. # ruff:ignore[missing-todo-link]
with django_assert_num_queries(31, exact=False): with django_assert_num_queries(31, exact=False):
response: _MonkeyPatchedWSGIResponse = client.get(url) response: _MonkeyPatchedWSGIResponse = client.get(url)

View file

@ -15,7 +15,7 @@ if TYPE_CHECKING:
@pytest.mark.django_db(transaction=True) @pytest.mark.django_db(transaction=True)
def test_0021_backfills_allowed_campaign_count() -> None: # noqa: PLR0914 def test_0021_backfills_allowed_campaign_count() -> None: # ruff:ignore[too-many-locals]
"""Migration 0021 should backfill cached allowed campaign counts.""" """Migration 0021 should backfill cached allowed campaign counts."""
migrate_from: list[tuple[str, str]] = [ migrate_from: list[tuple[str, str]] = [
("twitch", "0020_rewardcampaign_tw_reward_ends_starts_idx"), ("twitch", "0020_rewardcampaign_tw_reward_ends_starts_idx"),

View file

@ -2555,7 +2555,7 @@ class TestRewardCampaignViews:
game.owners.add(org) game.owners.add(org)
return game return game
def _create_reward_campaign( # noqa: PLR0913 def _create_reward_campaign( # ruff:ignore[too-many-arguments]
self, self,
twitch_id: str, twitch_id: str,
*, *,

View file

@ -196,7 +196,7 @@ def _build_breadcrumb_schema(items: list[dict[str, str | int]]) -> dict[str, Any
Returns: Returns:
BreadcrumbList schema dict. BreadcrumbList schema dict.
""" """
# TODO(TheLovinator): Replace dict with something more structured, like a dataclass or namedtuple, for better type safety and readability. # noqa: TD003 # TODO(TheLovinator): Replace dict with something more structured, like a dataclass or namedtuple, for better type safety and readability. # ruff:ignore[missing-todo-link]
breadcrumb_items: list[dict[str, str | int]] = [] breadcrumb_items: list[dict[str, str | int]] = []
for position, item in enumerate(items, start=1): for position, item in enumerate(items, start=1):
@ -384,7 +384,7 @@ def organization_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespon
# MARK: /campaigns/ # MARK: /campaigns/
def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0914 def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # ruff:ignore[too-many-locals]
"""Function-based view for drop campaigns list. """Function-based view for drop campaigns list.
Args: Args:
@ -483,7 +483,7 @@ def drop_campaign_list_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0
# MARK: /campaigns/<twitch_id>/ # MARK: /campaigns/<twitch_id>/
def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpResponse: # noqa: PLR0914 def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpResponse: # ruff:ignore[too-many-locals]
"""Function-based view for a drop campaign detail. """Function-based view for a drop campaign detail.
Args: Args:
@ -536,7 +536,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
reverse("twitch:campaign_detail", args=[campaign.twitch_id]), reverse("twitch:campaign_detail", args=[campaign.twitch_id]),
) )
# TODO(TheLovinator): If the campaign has specific allowed channels, we could list those as potential locations instead of just linking to Twitch homepage. # noqa: TD003 # TODO(TheLovinator): If the campaign has specific allowed channels, we could list those as potential locations instead of just linking to Twitch homepage. # ruff:ignore[missing-todo-link]
campaign_event: dict[str, Any] = { campaign_event: dict[str, Any] = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "Event", "@type": "Event",
@ -590,7 +590,7 @@ def drop_campaign_detail_view(request: HttpRequest, twitch_id: str) -> HttpRespo
campaign_schema: dict[str, Any] = campaign_event campaign_schema: dict[str, Any] = campaign_event
# Breadcrumb schema for navigation # Breadcrumb schema for navigation
# TODO(TheLovinator): We should have a game.get_display_name() method that encapsulates the logic of choosing between display_name, name, and twitch_id. # noqa: TD003 # TODO(TheLovinator): We should have a game.get_display_name() method that encapsulates the logic of choosing between display_name, name, and twitch_id. # ruff:ignore[missing-todo-link]
game_name: str = ( game_name: str = (
campaign.game.display_name or campaign.game.name or campaign.game.twitch_id campaign.game.display_name or campaign.game.name or campaign.game.twitch_id
) )
@ -702,7 +702,7 @@ class GameDetailView(DetailView):
"""Return game queryset optimized for the game detail page.""" """Return game queryset optimized for the game detail page."""
return Game.for_detail_view() return Game.for_detail_view()
def get_context_data(self, **kwargs) -> dict[str, Any]: # noqa: PLR0914 def get_context_data(self, **kwargs) -> dict[str, Any]: # ruff:ignore[too-many-locals]
"""Add additional context data. """Add additional context data.
Args: Args:
@ -849,7 +849,7 @@ def dashboard(request: HttpRequest) -> HttpResponse:
dashboard_data: dict[str, Any] = DropCampaign.dashboard_context(now) dashboard_data: dict[str, Any] = DropCampaign.dashboard_context(now)
# WebSite schema with SearchAction for sitelinks search box # WebSite schema with SearchAction for sitelinks search box
# TODO(TheLovinator): Should this be on all pages instead of just the dashboard? # noqa: TD003 # TODO(TheLovinator): Should this be on all pages instead of just the dashboard? # ruff:ignore[missing-todo-link]
website_schema: dict[str, str | dict[str, str | dict[str, str]]] = { website_schema: dict[str, str | dict[str, str | dict[str, str]]] = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "WebSite", "@type": "WebSite",
@ -1184,7 +1184,7 @@ class ChannelDetailView(DetailView):
channel: Channel = get_object_or_404(queryset, twitch_id=twitch_id) channel: Channel = get_object_or_404(queryset, twitch_id=twitch_id)
return channel return channel
def get_context_data(self, **kwargs) -> dict[str, Any]: # noqa: PLR0914 def get_context_data(self, **kwargs) -> dict[str, Any]: # ruff:ignore[too-many-locals]
"""Add additional context data. """Add additional context data.
Args: Args: