chore: konfiguracja ruff i poprawki wskazane przez lintera

Ruff jako linter i formatter, konfiguracja w pyproject.toml:

- reguły E, W, F, I, N, UP, B, C4, SIM, TID, BLE, RUF
- extend-immutable-calls dla fastapi.Depends, File i Query - wstrzykiwanie
  zależności opiera się na wywołaniach w wartościach domyślnych argumentów,
  więc bugbear B008 zgłaszał tam 66 fałszywych trafień
- zakaz importów względnych, aby ścieżki w pakiecie czytało się jednoznacznie
- katalog frontend wyłączony z analizy

Poprawki, które wskazał linter:

- Awaitable, Callable i AsyncIterator importowane z collections.abc zamiast
  z typing
- zbędny argument trybu w open(path, "r")
- contextlib.suppress zamiast try/except/pass przy anulowaniu zadania
- zdjęte dwie niepotrzebne dyrektywy noqa
- pięć linii przekraczających 100 znaków zawiniętych

Całość przepuszczona przez ruff format. Do .gitignore dochodzi .ruff_cache/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
lgooseandClaude Bot committed 2026-08-21 01:25:40 +02:00
1 parent ccfb0d2e34
commit 2a961e1a8a
12 files changed
+80 -30

No files matched your search

+3
View File
@@ -5,3 +5,6 @@ downloads/
.DS_Store .DS_Store
config/ config/
uploads/ uploads/
# Cache narzędzi deweloperskich
.ruff_cache/
+48
View File
@@ -0,0 +1,48 @@
[project]
name = "zyrarbot"
version = "2.0.0"
description = "Bot muzyczny Discord z panelem webowym"
requires-python = ">=3.13"
dynamic = ["dependencies"]
[tool.setuptools.dynamic]
dependencies = { file = ["requirements.txt"] }
[tool.setuptools.packages.find]
include = ["zyrarbot*"]
[tool.ruff]
target-version = "py313"
line-length = 100
extend-exclude = ["frontend"]
[tool.ruff.lint]
select = [
"E", # pycodestyle - błędy
"W", # pycodestyle - ostrzeżenia
"F", # pyflakes
"I", # isort - kolejność importów
"N", # pep8-naming
"UP", # pyupgrade
"B", # bugbear
"C4", # comprehensions
"SIM", # uproszczenia
"TID", # zakaz importów względnych
"BLE", # przechwytywanie ogólnych wyjątków
"RUF",
]
[tool.ruff.lint.flake8-bugbear]
# Wstrzykiwanie zależności w FastAPI opiera się na wywołaniach w wartościach
# domyślnych argumentów - to idiom biblioteki, nie pomyłka.
extend-immutable-calls = ["fastapi.Depends", "fastapi.File", "fastapi.Query"]
[tool.ruff.lint.flake8-tidy-imports]
ban-relative-imports = "all"
[tool.ruff.lint.isort]
known-first-party = ["zyrarbot", "tests"]
[tool.ruff.lint.per-file-ignores]
# Testy sięgają po prywatne pola atrap i celowo łamią kilka konwencji.
"tests/*" = ["SLF001"]
+3 -1
View File
@@ -21,7 +21,9 @@ class TrackFactory:
self._probe = probe or MediaProbe() self._probe = probe or MediaProbe()
self._ytdl = yt_dlp.YoutubeDL(YtdlOptions.EXTRACTOR) self._ytdl = yt_dlp.YoutubeDL(YtdlOptions.EXTRACTOR)
async def resolve(self, query: str, *, loop: asyncio.AbstractEventLoop | None = None) -> YTDLSource: async def resolve(
self, query: str, *, loop: asyncio.AbstractEventLoop | None = None
) -> YTDLSource:
"""Rozpoznaje, czy zapytanie wskazuje wgrany plik, czy zasób sieciowy.""" """Rozpoznaje, czy zapytanie wskazuje wgrany plik, czy zasób sieciowy."""
if not query.startswith("http") and self._uploads.exists(query): if not query.startswith("http") and self._uploads.exists(query):
return await self.from_file(self._uploads.resolve(query)) return await self.from_file(self._uploads.resolve(query))
+1 -3
View File
@@ -36,9 +36,7 @@ class ZyrarBot(commands.Bot):
if self.player is None: if self.player is None:
raise RuntimeError("MusicPlayer musi zostać przypisany przed startem bota") raise RuntimeError("MusicPlayer musi zostać przypisany przed startem bota")
await self.add_cog( await self.add_cog(MusicCog(self, self.player, self.tracks, self.uploads, self.settings))
MusicCog(self, self.player, self.tracks, self.uploads, self.settings)
)
if self.settings.guild_id: if self.settings.guild_id:
guild = discord.Object(id=self.settings.guild_id) guild = discord.Object(id=self.settings.guild_id)
+12 -12
View File
@@ -74,10 +74,10 @@ class MusicCog(commands.Cog, name="MusicBot"):
return False return False
return True return True
async def _enqueue_and_reply(self, interaction: discord.Interaction, track, *, prefix: str = "") -> None: async def _enqueue_and_reply(
result = await self.player.enqueue( self, interaction: discord.Interaction, track, *, prefix: str = ""
interaction.guild.id, track, channel=interaction.channel ) -> None:
) result = await self.player.enqueue(interaction.guild.id, track, channel=interaction.channel)
title = discord.utils.escape_markdown(track.title or "Nieznany tytuł") title = discord.utils.escape_markdown(track.title or "Nieznany tytuł")
action = "Dodano do kolejki" if result["status"] == "queued" else "Teraz odtwarzane" action = "Dodano do kolejki" if result["status"] == "queued" else "Teraz odtwarzane"
await interaction.followup.send(f"{action}{prefix}: {title}") await interaction.followup.send(f"{action}{prefix}: {title}")
@@ -161,7 +161,9 @@ class MusicCog(commands.Cog, name="MusicBot"):
for entry in matches[:AUTOCOMPLETE_LIMIT] for entry in matches[:AUTOCOMPLETE_LIMIT]
] ]
@app_commands.command(name="history", description="Pokaż historię ostatnio odtwarzanych utworów") @app_commands.command(
name="history", description="Pokaż historię ostatnio odtwarzanych utworów"
)
async def show_history(self, interaction: discord.Interaction) -> None: async def show_history(self, interaction: discord.Interaction) -> None:
entries = self.player.history(interaction.guild.id) entries = self.player.history(interaction.guild.id)
if not entries: if not entries:
@@ -263,9 +265,7 @@ class MusicCog(commands.Cog, name="MusicBot"):
voice_client.stop() voice_client.stop()
self.player.clear(guild_id) self.player.clear(guild_id)
await self.player.trigger_state_change() await self.player.trigger_state_change()
await interaction.response.send_message( await interaction.response.send_message("Zatrzymano odtwarzanie i wyczyszczono kolejkę.")
"Zatrzymano odtwarzanie i wyczyszczono kolejkę."
)
@app_commands.command(name="volume", description="Zmień głośność odtwarzacza") @app_commands.command(name="volume", description="Zmień głośność odtwarzacza")
@app_commands.describe(volume="Poziom głośności (1-100)") @app_commands.describe(volume="Poziom głośności (1-100)")
@@ -299,7 +299,9 @@ class MusicCog(commands.Cog, name="MusicBot"):
@app_commands.command(name="leave", description="Odłącz bota od kanału głosowego") @app_commands.command(name="leave", description="Odłącz bota od kanału głosowego")
async def leave(self, interaction: discord.Interaction) -> None: async def leave(self, interaction: discord.Interaction) -> None:
if interaction.guild.voice_client is None: if interaction.guild.voice_client is None:
await interaction.response.send_message("Nie jestem na kanale głosowym.", ephemeral=True) await interaction.response.send_message(
"Nie jestem na kanale głosowym.", ephemeral=True
)
return return
await interaction.guild.voice_client.disconnect() await interaction.guild.voice_client.disconnect()
self.player.clear(interaction.guild.id) self.player.clear(interaction.guild.id)
@@ -327,9 +329,7 @@ class MusicCog(commands.Cog, name="MusicBot"):
await self.player.trigger_state_change() await self.player.trigger_state_change()
description = ( description = (
"włączone (utwory po zakończeniu wracają na koniec kolejki)" "włączone (utwory po zakończeniu wracają na koniec kolejki)" if enabled else "wyłączone"
if enabled
else "wyłączone"
) )
await interaction.response.send_message(f"🔁 Zapętlanie całej kolejki: **{description}**.") await interaction.response.send_message(f"🔁 Zapętlanie całej kolejki: **{description}**.")
+3 -4
View File
@@ -2,7 +2,8 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
from typing import Any, Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import Any
import discord import discord
from discord.ext import commands from discord.ext import commands
@@ -266,9 +267,7 @@ class MusicPlayer:
def _schedule_next(self, guild_id: int, error: Exception | None) -> None: def _schedule_next(self, guild_id: int, error: Exception | None) -> None:
if error: if error:
logger.error("Odtwarzanie zakończone błędem na serwerze %s: %s", guild_id, error) logger.error("Odtwarzanie zakończone błędem na serwerze %s: %s", guild_id, error)
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(self.play_next(guild_id, advance=True), self._bot.loop)
self.play_next(guild_id, advance=True), self._bot.loop
)
def _record_history(self, guild_id: int, track: YTDLSource) -> None: def _record_history(self, guild_id: int, track: YTDLSource) -> None:
if not track.title: if not track.title:
+3 -1
View File
@@ -22,7 +22,9 @@ class TrackEmbedFactory:
embed.set_footer(text=f"Pozycja w kolejce: {position} / {total}") embed.set_footer(text=f"Pozycja w kolejce: {position} / {total}")
return embed return embed
def _base(self, track: YTDLSource, color: discord.Color, *, prefix: str = "🎵") -> discord.Embed: def _base(
self, track: YTDLSource, color: discord.Color, *, prefix: str = "🎵"
) -> discord.Embed:
title = discord.utils.escape_markdown(track.title or "Nieznany tytuł") title = discord.utils.escape_markdown(track.title or "Nieznany tytuł")
url = track.display_url url = track.display_url
icon = "📁" if track.is_local else prefix icon = "📁" if track.is_local else prefix
+1 -1
View File
@@ -22,7 +22,7 @@ class JsonFileStore:
if not os.path.exists(self._path): if not os.path.exists(self._path):
return {} return {}
try: try:
with open(self._path, "r", encoding="utf-8") as handle: with open(self._path, encoding="utf-8") as handle:
data = json.load(handle) data = json.load(handle)
except (OSError, ValueError) as error: except (OSError, ValueError) as error:
logger.error("Nie udało się odczytać %s: %s", self._path, error) logger.error("Nie udało się odczytać %s: %s", self._path, error)
+1 -1
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
import logging import logging
import os import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from typing import AsyncIterator
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
+2 -2
View File
@@ -47,7 +47,7 @@ async def play(
try: try:
track = await tracks.resolve(request.query) track = await tracks.resolve(request.query)
result = await player.enqueue(guild.id, track, from_web=True) result = await player.enqueue(guild.id, track, from_web=True)
except Exception as error: # noqa: BLE001 - treść błędu trafia do panelu except Exception as error:
logger.error("Nie udało się dodać utworu z panelu: %s", error) logger.error("Nie udało się dodać utworu z panelu: %s", error)
raise HTTPException(status_code=500, detail=str(error)) from error raise HTTPException(status_code=500, detail=str(error)) from error
@@ -73,7 +73,7 @@ async def play_file(
uploads.touch(request.filename) uploads.touch(request.filename)
track = await tracks.from_file(uploads.resolve(request.filename)) track = await tracks.from_file(uploads.resolve(request.filename))
result = await player.enqueue(guild.id, track, from_web=True) result = await player.enqueue(guild.id, track, from_web=True)
except Exception as error: # noqa: BLE001 except Exception as error:
logger.error("Nie udało się odtworzyć pliku z panelu: %s", error) logger.error("Nie udało się odtworzyć pliku z panelu: %s", error)
raise HTTPException(status_code=500, detail=str(error)) from error raise HTTPException(status_code=500, detail=str(error)) from error
+2 -3
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import contextlib
import logging import logging
from zyrarbot.web.connection_manager import ConnectionManager from zyrarbot.web.connection_manager import ConnectionManager
@@ -35,10 +36,8 @@ class StatusBroadcaster:
if self._task is None: if self._task is None:
return return
self._task.cancel() self._task.cancel()
try: with contextlib.suppress(asyncio.CancelledError):
await self._task await self._task
except asyncio.CancelledError:
pass
self._task = None self._task = None
async def _loop(self) -> None: async def _loop(self) -> None:
+1 -2
View File
@@ -75,8 +75,7 @@ class StatusService:
str(notification_channel.id) if notification_channel else None str(notification_channel.id) if notification_channel else None
), ),
"history": [ "history": [
entry.to_dict() entry.to_dict() for entry in self._player.history(guild.id)[:HISTORY_PAYLOAD_LIMIT]
for entry in self._player.history(guild.id)[:HISTORY_PAYLOAD_LIMIT]
], ],
} }