Skip to content

Commit

Permalink
feat: add astral ruff commands (#63)
Browse files Browse the repository at this point in the history
* feat: add astral ruff commands

* fix: make non blocking

* chore: adjust docstring

* docs: update docs for astral

fix(docs): fix broken refs

* Dropdown and some type changes (#64)

* feat(astral-ruff): Drop down support for rules and faster rule lookup

* fix?(astral-ruff): Chunking

* chore(astral-ruff): replace `query_ruff_rule` with `query_all_ruff_rules` in `__all__`

* oopsie:(astral-ruff): Missed call to `format_ruff_rule`

* docs: fixed

* Update src/byte/lib/utils.py

---------

Co-authored-by: Alc-Alc <alc@localhost>
Co-authored-by: Jacob Coffee <jacob@z7x.org>

---------

Co-authored-by: Alc-Alc <45509143+Alc-Alc@users.noreply.github.com>
Co-authored-by: Alc-Alc <alc@localhost>
  • Loading branch information
3 people committed Feb 27, 2024
1 parent f9ff9ad commit e69751b
Show file tree
Hide file tree
Showing 19 changed files with 270 additions and 35 deletions.
6 changes: 6 additions & 0 deletions docs/byte/api/plugins/astral.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
======
astral
======

.. automodule:: src.byte.plugins.astral
:members:
6 changes: 6 additions & 0 deletions docs/byte/api/plugins/events.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
======
events
======

.. automodule:: src.byte.plugins.events
:members:
4 changes: 2 additions & 2 deletions docs/byte/api/plugins/github.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
======
events
github
======

.. automodule:: src.byte.plugins.events
.. automodule:: src.byte.plugins.github
:members:
8 changes: 4 additions & 4 deletions docs/byte/api/plugins/guild.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
======
forums
======
=====
guild
=====

.. automodule:: src.byte.plugins.forums
.. automodule:: src.byte.plugins.guild
:members:
2 changes: 2 additions & 0 deletions docs/byte/api/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This includes things like commands, event handlers, and other features.
:caption: Plugins

admin
astral
events
general
github
guild
Expand Down
16 changes: 9 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -206,23 +206,25 @@ warn_untyped_fields = true
strict-imports = false

[tool.ruff]
select = ["ALL"]
ignore = ["ANN101", "ANN102", "ANN401", "PLR0913", "RUF012", "COM812", "ISC001", "ERA001", "TD", "FIX002"]
line-length = 120
src = ["src", "tests"]
target-version = "py311"

[tool.ruff.lint]
select = ["ALL"]
ignore = ["ANN101", "ANN102", "ANN401", "PLR0913", "RUF012", "COM812", "ISC001", "ERA001", "TD", "FIX002"]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

[tool.ruff.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.mccabe]
[tool.ruff.lint.mccabe]
max-complexity = 12

[tool.ruff.pep8-naming]
[tool.ruff.lint.pep8-naming]
classmethod-decorators = [
"classmethod",
"pydantic.validator",
Expand All @@ -233,10 +235,10 @@ classmethod-decorators = [
"sqlalchemy.orm.declared_attr",
]

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["src", "tests"]

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
# Tests can use magic values, assertions, and relative imports
"**/*.*" = [
"ANN101",
Expand Down
2 changes: 1 addition & 1 deletion src/byte/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from __future__ import annotations

from byte import bot, lib
from byte.lib.logging import setup_logging
from byte.lib.log import setup_logging

__all__ = ["bot", "lib"]

Expand Down
2 changes: 1 addition & 1 deletion src/byte/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from dotenv import load_dotenv

from byte.lib import settings
from byte.lib.logging import get_logger
from byte.lib.log import get_logger

__all__ = [
"Byte",
Expand Down
3 changes: 2 additions & 1 deletion src/byte/lib/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
pypi_litestar: Final = "https://pypi.org/project/litestar/"
pypi_polyfactory: Final = "https://pypi.org/project/polyfactory/"
mcve: Final = "https://stackoverflow.com/help/minimal-reproducible-example"
paste: Final = "https://paste.pythondiscord.com"
pastebin: Final = "https://paste.pythondiscord.com"
markdown_guide: Final = "https://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-#h_01GY0DAKGXDEHE263BCAYEGFJA"

# --- Assets
litestar_logo_white: Final = "https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20PNG%20-%20Transparent/Badge%20-%20White.png"
litestar_logo_yellow: Final = "https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20PNG%20-%20Transparent/Badge%20-%20Blue%20and%20Yellow.png"
ruff_logo: Final = "https://raw.githubusercontent.com/JacobCoffee/byte/main/assets/ruff.png"

# --- Channel IDs
litestar_help_channel: Final = 1064114019373432912
File renamed without changes.
8 changes: 4 additions & 4 deletions src/byte/lib/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@

load_dotenv()

DEFAULT_MODULE_NAME: Final = "src"
DEFAULT_MODULE_NAME: str = "src"
BASE_DIR: Final = utils.module_to_os_path(DEFAULT_MODULE_NAME)
PLUGINS_DIR: Final = utils.module_to_os_path("byte.plugins")


class DiscordSettings(BaseSettings):
"""Discord Settings."""

model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="DISCORD_")
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="DISCORD_", extra="ignore")

TOKEN: str
"""Discord API token."""
Expand Down Expand Up @@ -91,7 +91,7 @@ def assemble_presence_url(cls, value: str) -> str: # noqa: ARG003
class LogSettings(BaseSettings):
"""Logging config for the Project."""

model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="LOG_")
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="LOG_", extra="ignore")

LEVEL: int = 20
"""Stdlib log levels.
Expand All @@ -113,7 +113,7 @@ class LogSettings(BaseSettings):
class ProjectSettings(BaseSettings):
"""Project Settings."""

model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra="allow")
model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra="ignore")

DEBUG: bool = False
"""Run app with ``debug=True``."""
Expand Down
136 changes: 133 additions & 3 deletions src/byte/lib/utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
"""Byte utilities."""
from __future__ import annotations

from typing import TYPE_CHECKING

import json
import re
import subprocess
from itertools import islice
from typing import TYPE_CHECKING, TypedDict, TypeVar

import httpx
from anyio import run_process
from discord.ext import commands
from ruff.__main__ import find_ruff_bin # type: ignore[import-untyped]

from byte.lib import settings
from byte.lib.common import pastebin

if TYPE_CHECKING:
from collections.abc import Iterable
from typing import Any

from discord.ext.commands import Context
from discord.ext.commands._types import Check


_T = TypeVar("_T")


class BaseRuffRule(TypedDict):
name: str
summary: str
fix: str
explanation: str


class RuffRule(BaseRuffRule):
code: str
linter: str
message_formats: list[str]
preview: bool


class FormattedRuffRule(BaseRuffRule):
rule_link: str


__all__ = (
"is_byte_dev",
"linker",
Expand All @@ -25,6 +56,10 @@
"mention_custom_emoji_animated",
"mention_timestamp",
"mention_guild_navigation",
"format_ruff_rule",
"query_all_ruff_rules",
"run_ruff_format",
"paste",
)


Expand All @@ -47,7 +82,7 @@ async def predicate(ctx: Context) -> bool:
if await ctx.bot.is_owner(ctx.author) or ctx.author.id == settings.discord.DEV_USER_ID:
return True

return any(role.name == "byte-dev" for role in ctx.author.roles)
return any(role.name == "byte-dev" for role in ctx.author.roles) # type: ignore[reportAttributeAccessIssue]

return commands.check(predicate)

Expand Down Expand Up @@ -178,3 +213,98 @@ def mention_guild_navigation(guild_nav_type: str, guild_element_id: int) -> str:
A formatted string that mentions the guild navigation element.
"""
return f"<{guild_element_id}:{guild_nav_type}>"


def format_ruff_rule(rule_data: RuffRule) -> FormattedRuffRule:
"""Format ruff rule data for embed-friendly output and append rule link.
Args:
rule_data: The ruff rule data.
Returns:
FormattedRuffRule: The formatted rule data.
"""
explanation_formatted = re.sub(r"## (.+)", r"**\1**", rule_data["explanation"])
rule_name = rule_data["code"]
rule_link = f"https://docs.astral.sh/ruff/rules/#{rule_name}"

return {
"name": rule_data.get("name", "No name available"),
"summary": rule_data.get("summary", "No summary available"),
"explanation": explanation_formatted,
"fix": rule_data.get("fix", "No fix available"),
"rule_link": rule_link,
}


async def query_all_ruff_rules() -> list[RuffRule]:
"""Query all Ruff linting rules.
Returns:
list[RuffRule]: All ruff rules
"""
_ruff = find_ruff_bin()
try:
result = await run_process([_ruff, "rule", "--all", "--output-format", "json"])
except subprocess.CalledProcessError as e:
stderr = getattr(e, "stderr", b"").decode()
msg = f"Error while querying all rules: {stderr}"
raise ValueError(msg) from e
else:
return json.loads(result.stdout.decode())


def run_ruff_format(code: str) -> str:
"""Formats code using Ruff.
Args:
code: The code to format.
Returns:
str: The formatted code.
"""
result = subprocess.run(
["ruff", "format", "-"], # noqa: S603, S607
input=code,
capture_output=True,
text=True,
check=False,
)
return result.stdout if result.returncode == 0 else code


async def paste(code: str) -> str:
"""Uploads the given code to paste.pythondiscord.com.
Args:
code: The formatted code to upload.
Returns:
str: The URL of the uploaded paste.
"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{pastebin}/api/v1/paste",
json={
"expiry": "1day",
"files": [{"name": "byte-bot_formatted_code.py", "lexer": "python", "content": code}],
},
)
response_data = response.json()
paste_link = response_data.get("link")
return paste_link or "Failed to upload formatted code."


def chunk_sequence(sequence: Iterable[_T], size: int) -> Iterable[tuple[_T, ...]]:
"""Naïve chunking of an iterable
Args:
sequence (Iterable[_T]): Iterable to chunk
size (int): Size of chunk
Yields:
Iterable[tuple[_T, ...]]: An n-tuple that contains chunked data
"""
_sequence = iter(sequence)
while chunk := tuple(islice(_sequence, size)):
yield chunk
Loading

0 comments on commit e69751b

Please sign in to comment.