Skip to content

feat: Setup a fully featured test setup and add initial tests on utils #39

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 67 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
17766df
Move utils.py to utils/
Paillat-dev May 29, 2025
195e2a0
:fire: Remove `filter_params`
Paillat-dev May 29, 2025
fafa149
:recycle: Merge `time_snowflake` and `generate_snowflake`, move `basi…
Paillat-dev May 29, 2025
e2d8eb1
:recycle: Merge `time_snowflake` and `generate_snowflake`, move `basi…
Paillat-dev May 29, 2025
86bd168
:fire: Remove `utils.sleep_until`
Paillat-dev May 29, 2025
23c2c49
chore: Start migration to uv & ruff & hatch (#4)
Paillat-dev May 29, 2025
97ccda5
Setup CHANGELOG.md (#6)
Paillat-dev May 29, 2025
f2a4eb6
chore: update docs workflows to use 'uv' for dependency management (#33)
Paillat-dev May 30, 2025
40e20c0
:fire: Move stuff to private
Paillat-dev May 31, 2025
6e5ccb5
Merge branch 'master' into refactor-utils
Paillat-dev May 31, 2025
1da5de0
:refactor: move parse_time function to private utils and update refer…
Paillat-dev Jun 24, 2025
88180b2
:memo: update CHANGELOG to reflect utility function changes
Paillat-dev Jun 24, 2025
b5c8a58
:art: Format
Paillat-dev Jun 24, 2025
0cdcb31
:recycle: move deprecation utilities to private utils and update refe…
Paillat-dev Jun 24, 2025
09a11a9
:recycle: move snowflake_time function to public.py
Paillat-dev Jun 24, 2025
4a2753c
:recycle: move oauth_url and Undefined class to public.py; update imp…
Paillat-dev Jun 24, 2025
ca89539
Merge branch 'master' into refactor-utils
Paillat-dev Jun 24, 2025
1cf53ab
:memo: remove deprecated utility functions from documentation
Paillat-dev Jun 24, 2025
2ac8152
:memo: remove (re)moved utility functions from documentation
Paillat-dev Jun 24, 2025
24a367c
:memo: add utils.resolve_template to changelog and remove from docume…
Paillat-dev Jun 24, 2025
0c6270a
:bug: fix import path for warn_deprecated utility function
Paillat-dev Jun 24, 2025
84ff9df
:refactor: reorganize utility function imports and move evaluate_anno…
Paillat-dev Jun 24, 2025
8474f05
:recycle: update import paths for utility functions to use relative i…
Paillat-dev Jun 24, 2025
c8ddcb5
:recycle: move delay_task function to private
Paillat-dev Jul 7, 2025
8a02d54
:recycle: removed `utils.get` in favor of `utils.find`
Paillat-dev Jul 7, 2025
39aef0f
:recycle: removed `utils._unique`
Paillat-dev Jul 7, 2025
6b0379c
:recycle: move `async_all` to private
Paillat-dev Jul 7, 2025
cc8f27f
:recycle: move `maybe_coroutine` to private
Paillat-dev Jul 7, 2025
1d70dec
:recycle: rename `maybe_coroutine` to `maybe_awaitable`
Paillat-dev Jul 7, 2025
b2b8625
:recycle: move `sane_wait_for` to private
Paillat-dev Jul 7, 2025
d0a094d
:recycle: move `format_dt` to public
Paillat-dev Jul 7, 2025
fb554fd
:recycle: remove `as_chunks` function
Paillat-dev Jul 7, 2025
1a551cb
:memo: update `utils.sleep_until` and `utils.parse_time` changelog to…
Paillat-dev Jul 7, 2025
295ed46
:recycle: move `compute_timedelta` function
Paillat-dev Jul 7, 2025
4afe54a
:recycle: move `valid_icon_size` to `asset.py`
Paillat-dev Jul 7, 2025
a021450
:recycle: refactor `utils.get` to `utils.find` across multiple files
Paillat-dev Jul 7, 2025
c6f7914
:recycle: refactor markdown and mention handling functions in `__init…
Paillat-dev Jul 7, 2025
c29cdc2
:recycle: move SnowflakeList to `private.py`
Paillat-dev Jul 7, 2025
6c57d5e
:recycle: move `find` function from `__init__.py` to `public.py`
Paillat-dev Jul 7, 2025
d079c7c
:recycle: move `copy_doc` to private
Paillat-dev Jul 7, 2025
611c7c7
:recycle: refactor `get` to `find` in onboarding and sticker modules
Paillat-dev Jul 7, 2025
cd4e436
:bug: fix `copy_doc` decorator usage in context.py
Paillat-dev Jul 7, 2025
ad777e4
Merge branch 'master' into refactor-utils
Paillat-dev Jul 7, 2025
aec45a2
:recycle: move SequenceProxy to private module
Paillat-dev Jul 7, 2025
9755fe8
:recycle: move cached_slot_property to private
Paillat-dev Jul 7, 2025
006a7f8
:rotating_light: add noqa comments to prevent linting errors
Paillat-dev Jul 7, 2025
d5263c7
:recycle: move get_slots function to private module
Paillat-dev Jul 7, 2025
44f753e
:recycle: refactor JSON serialization functions to private module
Paillat-dev Jul 7, 2025
c961e36
:recycle: replace custom cached_property implementation with functool…
Paillat-dev Jul 7, 2025
a4bda5f
:pencil2: fix typo in CHANGELOG-V3.md
Paillat-dev Jul 7, 2025
b81d2d5
:heavy_minus_sign: remove unused dependencies from pyproject.toml and…
Paillat-dev Jul 7, 2025
a84e06d
:coffin: remove test.py
Paillat-dev Jul 7, 2025
330161c
:recycle: remove duplicate import of raw_mentions in __init__.py
Paillat-dev Jul 7, 2025
b3f919e
:bug: fix raw_role_mentions import in utils/__init__.py
Paillat-dev Jul 8, 2025
8d76921
:bug: fix dictionary access in test_typing_annotated.py to be type safe
Paillat-dev Jul 8, 2025
77bf9c3
:coffin: Remove dead tests
Paillat-dev Jul 8, 2025
c7a7a77
Merge branch 'refactor-utils' into feat/tests
Paillat-dev Jul 8, 2025
d93aa3d
:white_check_mark: add tox configuration and update dependencies in p…
Paillat-dev Jul 8, 2025
15449c6
:coffin: remove unused reveal_type calls in private.py
Paillat-dev Jul 8, 2025
a3b6eb9
:white_check_mark: add test workflow to GitHub Actions for multiple O…
Paillat-dev Jul 8, 2025
f3a8f1e
:pushpin: move pytest and pytest-asyncio to test dependencies in pypr…
Paillat-dev Jul 8, 2025
e2a4367
:white_check_mark: add unit tests for markdown related utils
Paillat-dev Jul 8, 2025
414e0fe
:white_check_mark: add unit tests for format_dt
Paillat-dev Jul 8, 2025
3870d73
:white_check_mark: add unit tests for snowflake generation and time c…
Paillat-dev Jul 8, 2025
96e5cce
:white_check_mark: add unit tests for discord.utils.find functionality
Paillat-dev Jul 8, 2025
352d1b6
:page_facing_up: add license headers to test files
Paillat-dev Jul 8, 2025
e58e310
:bug: fix test for find function to use == instead of is
Paillat-dev Jul 8, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/lib-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ jobs:
uv run codespell --ignore-words-list="groupt,nd,ot,ro,falsy,BU" \
--exclude-file=".github/workflows/codespell.yml"
ruff:
if: ${{ github.event_name != 'schedule' }}
runs-on: ubuntu-latest
steps:
- name: "Checkout Repository"
Expand Down Expand Up @@ -93,3 +94,29 @@ jobs:
run: mkdir -p -v .mypy_cache
- name: "Run mypy"
run: uv run mypy --non-interactive discord/
tests:
if: ${{ github.event_name != 'schedule' }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.13', '3.12', '3.11', '3.10']
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4

- name: "Setup Python"
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: "Install uv"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Sync dependencies
run: uv sync --no-python-downloads --group dev

- name: "Run tests"
run: uv run tox
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
- id: end-of-file-fixer
exclude: \.(po|pot|yml|yaml)$
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.9
rev: v0.12.0
hooks:
- id: ruff
args: [ --fix ]
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG-V3.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,17 @@ release.
### Deprecated

### Removed

- `utils.filter_params`
- `utils.sleep_until` use `asyncio.sleep` combined with `datetime.datetime` instead
- `utils.compute_timedelta` use the `datetime` module instead
- `utils.resolve_invite`
- `utils.resolve_template`
- `utils.parse_time` use `datetime.datetime.fromisoformat` instead
- `utils.time_snowflake` use `utils.generate_snowflake` instead
- `utils.warn_deprecated`
- `utils.deprecated`
- `utils.get` use `utils.find` with `lambda i: i.attr == val` instead
- `AsyncIterator.get` use `AsyncIterator.find` with `lambda i: i.attr == val` instead
- `utils.as_chunks` use `itertools.batched` on Python 3.12+ or your own implementation
instead
2 changes: 1 addition & 1 deletion discord/__version.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

from typing import Literal, NamedTuple

from .utils import deprecated
from .utils.private import deprecated
from ._version import __version__, __version_tuple__


Expand Down
5 changes: 3 additions & 2 deletions discord/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
runtime_checkable,
)

from .utils.private import warn_deprecated
from . import utils
from .context_managers import Typing
from .enums import ChannelType
Expand Down Expand Up @@ -724,7 +725,7 @@ def permissions_for(self, obj: Member | Role, /) -> Permissions:
if obj.is_default():
return base

overwrite = utils.get(self._overwrites, type=_Overwrites.ROLE, id=obj.id)
overwrite = utils.find(lambda o: o.type == _Overwrites.ROLE and o.id == obj.id, self._overwrites)
if overwrite is not None:
base.handle_overwrite(overwrite.allow, overwrite.deny)

Expand Down Expand Up @@ -1529,7 +1530,7 @@ async def send(
from .message import MessageReference # noqa: PLC0415

if not isinstance(reference, MessageReference):
utils.warn_deprecated(
warn_deprecated(
f"Passing {type(reference).__name__} to reference",
"MessageReference",
"2.7",
Expand Down
4 changes: 2 additions & 2 deletions discord/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .colour import Colour
from .enums import ActivityType, try_enum
from .partial_emoji import PartialEmoji
from .utils import _get_as_snowflake
from .utils.private import get_as_snowflake

__all__ = (
"BaseActivity",
Expand Down Expand Up @@ -226,7 +226,7 @@ def __init__(self, **kwargs):
self.timestamps: ActivityTimestamps = kwargs.pop("timestamps", {})
self.assets: ActivityAssets = kwargs.pop("assets", {})
self.party: ActivityParty = kwargs.pop("party", {})
self.application_id: int | None = _get_as_snowflake(kwargs, "application_id")
self.application_id: int | None = get_as_snowflake(kwargs, "application_id")
self.url: str | None = kwargs.pop("url", None)
self.flags: int = kwargs.pop("flags", 0)
self.sync_id: str | None = kwargs.pop("sync_id", None)
Expand Down
8 changes: 5 additions & 3 deletions discord/appinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@

from typing import TYPE_CHECKING

from .utils.private import warn_deprecated
from .utils.private import get_as_snowflake
from . import utils
from .asset import Asset
from .permissions import Permissions
Expand Down Expand Up @@ -200,9 +202,9 @@ def __init__(self, state: ConnectionState, data: AppInfoPayload):
self._summary: str = data["summary"]
self.verify_key: str = data["verify_key"]

self.guild_id: int | None = utils._get_as_snowflake(data, "guild_id")
self.guild_id: int | None = get_as_snowflake(data, "guild_id")

self.primary_sku_id: int | None = utils._get_as_snowflake(data, "primary_sku_id")
self.primary_sku_id: int | None = get_as_snowflake(data, "primary_sku_id")
self.slug: str | None = data.get("slug")
self._cover_image: str | None = data.get("cover_image")
self.terms_of_service_url: str | None = data.get("terms_of_service_url")
Expand Down Expand Up @@ -261,7 +263,7 @@ def summary(self) -> str | None:
.. versionadded:: 1.3
.. deprecated:: 2.7
"""
utils.warn_deprecated(
warn_deprecated(
"summary",
"description",
reference="https://discord.com/developers/docs/resources/application#application-object-application-structure",
Expand Down
9 changes: 7 additions & 2 deletions discord/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
MISSING = utils.MISSING


def _valid_icon_size(size: int) -> bool:
"""Icons must be power of 2 within [16, 4096]."""
return not size & (size - 1) and 4096 >= size >= 16


class AssetMixin:
url: str
_state: Any | None
Expand Down Expand Up @@ -371,7 +376,7 @@ def replace(
url = url.with_path(f"{path}.{static_format}")

if size is not MISSING:
if not utils.valid_icon_size(size):
if not _valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")
url = url.with_query(size=size)
else:
Expand All @@ -398,7 +403,7 @@ def with_size(self, size: int, /) -> Asset:
InvalidArgument
The asset had an invalid size.
"""
if not utils.valid_icon_size(size):
if not _valid_icon_size(size):
raise InvalidArgument("size must be a power of 2 between 16 and 4096")

url = str(yarl.URL(self._url).with_query(size=size))
Expand Down
18 changes: 10 additions & 8 deletions discord/audit_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, TypeVar
from functools import cached_property

from .utils.private import get_as_snowflake
from . import enums, utils
from .asset import Asset
from .automod import AutoModAction, AutoModTriggerMetadata
Expand Down Expand Up @@ -559,21 +561,21 @@ def _from_data(self, data: AuditLogEntryPayload) -> None:
# into meaningful data when requested
self._changes = data.get("changes", [])

self.user = self._get_member(utils._get_as_snowflake(data, "user_id")) # type: ignore
self._target_id = utils._get_as_snowflake(data, "target_id")
self.user = self._get_member(get_as_snowflake(data, "user_id")) # type: ignore
self._target_id = get_as_snowflake(data, "target_id")

def _get_member(self, user_id: int) -> Member | User | None:
return self.guild.get_member(user_id) or self._users.get(user_id)

def __repr__(self) -> str:
return f"<AuditLogEntry id={self.id} action={self.action} user={self.user!r}>"

@utils.cached_property
@cached_property
def created_at(self) -> datetime.datetime:
"""Returns the entry's creation time in UTC."""
return utils.snowflake_time(self.id)

@utils.cached_property
@cached_property
def target(
self,
) -> (
Expand All @@ -597,24 +599,24 @@ def target(
else:
return converter(self._target_id)

@utils.cached_property
@property
def category(self) -> enums.AuditLogActionCategory:
"""The category of the action, if applicable."""
return self.action.category

@utils.cached_property
@cached_property
def changes(self) -> AuditLogChanges:
"""The list of changes this entry has."""
obj = AuditLogChanges(self, self._changes, state=self._state)
del self._changes
return obj

@utils.cached_property
@property
def before(self) -> AuditLogDiff:
"""The target's prior state."""
return self.changes.before

@utils.cached_property
@property
def after(self) -> AuditLogDiff:
"""The target's subsequent state."""
return self.changes.after
Expand Down
25 changes: 12 additions & 13 deletions discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
from .shard import AutoShardedClient
from .types import interactions
from .user import User
from .utils import MISSING, async_all, find, get
from .utils import MISSING, find
from .utils.private import async_all

if TYPE_CHECKING:
from .member import Member
Expand Down Expand Up @@ -216,13 +217,13 @@ def get_application_command(
return command
elif (names := name.split())[0] == command.name and isinstance(command, SlashCommandGroup):
while len(names) > 1:
command = get(commands, name=names.pop(0))
command = find(lambda c: c.name == names.pop(0), commands)
if not isinstance(command, SlashCommandGroup) or (
guild_ids is not None and command.guild_ids != guild_ids
):
return
commands = command.subcommands
command = get(commands, name=names.pop())
command = find(lambda c: c.name == names.pop(), commands)
if not isinstance(command, type) or (guild_ids is not None and command.guild_ids != guild_ids):
return
return command
Expand Down Expand Up @@ -357,7 +358,7 @@ def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool:

# Now let's see if there are any commands on discord that we need to delete
for cmd, value_ in registered_commands_dict.items():
match = get(pending, name=value_["name"])
match = find(lambda c: c.name == value_["name"], pending)
if match is None:
# We have this command registered but not in our list
return_value.append(
Expand Down Expand Up @@ -515,8 +516,9 @@ def register(
}
)
continue
# We can assume the command item is a command, since it's only a string if action is delete
match = get(pending, name=cmd["command"].name, type=cmd["command"].type)
# We can assume the command item is a com
# mand, since it's only a string if action is delete
match = find(lambda c: c.name == cmd["command"].name and c.type == cmd["command"].type, pending)
if match is None:
continue
if cmd["action"] == "edit":
Expand Down Expand Up @@ -605,10 +607,9 @@ def register(
registered = await register("bulk", data, guild_id=guild_id)

for i in registered:
cmd = get(
cmd = find(
lambda c: c.name == i["name"] and c.type == i.get("type"),
self.pending_application_commands,
name=i["name"],
type=i.get("type"),
)
if not cmd:
raise ValueError(f"Registered command {i['name']}, type {i.get('type')} not found in pending commands")
Expand Down Expand Up @@ -712,11 +713,9 @@ async def on_connect():
registered_guild_commands[guild_id] = app_cmds

for i in registered_commands:
cmd = get(
cmd = find(
lambda c: c.name == i["name"] and c.guild_ids is None and c.type == i.get("type"),
self.pending_application_commands,
name=i["name"],
guild_ids=None,
type=i.get("type"),
)
if cmd:
cmd.id = i["id"]
Expand Down
Loading