Skip to content

Commit ca172af

Browse files
Merge pull request #93 from discord-modmail/feat/utils-time
Feat/utils time add discord timestamp utils
2 parents 1a77549 + b721e3a commit ca172af

File tree

4 files changed

+241
-0
lines changed

4 files changed

+241
-0
lines changed

modmail/utils/time.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import datetime
2+
import enum
3+
import typing
4+
5+
import arrow
6+
7+
8+
class TimeStampEnum(enum.Enum):
9+
"""
10+
Timestamp modes for discord.
11+
12+
Full docs on this format are viewable here:
13+
https://discord.com/developers/docs/reference#message-formatting
14+
"""
15+
16+
# fmt: off
17+
SHORT_TIME = "t" # 16:20
18+
LONG_TIME = "T" # 16:20:30
19+
SHORT_DATE = "d" # 20/04/2021
20+
LONG_DATE = "D" # 20 April 2021
21+
SHORT_DATE_TIME = "f" # 20 April 2021 16:20
22+
LONG_DATE_TIME = "F" # Tuesday, 20 April 2021 16:20
23+
RELATIVE_TIME = "R" # 2 months ago
24+
25+
# fmt: on
26+
# DEFAULT alised to the default, so for all purposes, it behaves like SHORT_DATE_TIME, including the name
27+
DEFAULT = SHORT_DATE_TIME
28+
29+
30+
TypeTimes = typing.Union[arrow.Arrow, datetime.datetime]
31+
32+
33+
def get_discord_formatted_timestamp(
34+
timestamp: TypeTimes, format: TimeStampEnum = TimeStampEnum.DEFAULT
35+
) -> str:
36+
"""
37+
Return a discord formatted timestamp from a datetime compatiable datatype.
38+
39+
`format` must be an enum member of TimeStampEnum. Default style is SHORT_DATE_TIME
40+
"""
41+
return f"<t:{int(timestamp.timestamp())}:{format.value}>"

tests/modmail/extensions/utils/__init__.py

Whitespace-only changes.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import inspect
2+
import typing
3+
import unittest.mock
4+
5+
import discord
6+
import pytest
7+
from discord.ext import commands
8+
9+
from modmail.extensions.utils import error_handler
10+
from modmail.extensions.utils.error_handler import ErrorHandler
11+
from tests import mocks
12+
13+
14+
@pytest.fixture
15+
def cog():
16+
"""Pytest fixture for error_handler."""
17+
return ErrorHandler(mocks.MockBot())
18+
19+
20+
@pytest.fixture
21+
def ctx():
22+
"""Pytest fixture for MockContext."""
23+
return mocks.MockContext(channel=mocks.MockTextChannel())
24+
25+
26+
def test_error_embed():
27+
"""Test the error embed method creates the correct embed."""
28+
title = "Something very drastic went very wrong!"
29+
message = "seven southern seas are ready to collapse."
30+
embed = ErrorHandler.error_embed(title=title, message=message)
31+
32+
assert embed.title == title
33+
assert embed.description == message
34+
assert embed.colour == error_handler.ERROR_COLOUR
35+
36+
37+
@pytest.mark.parametrize(
38+
["exception_or_str", "expected_str"],
39+
[
40+
[commands.NSFWChannelRequired(mocks.MockTextChannel()), "NSFW Channel Required"],
41+
[commands.CommandNotFound(), "Command Not Found"],
42+
["someWEIrdName", "some WE Ird Name"],
43+
],
44+
)
45+
def test_get_title_from_name(exception_or_str: typing.Union[Exception, str], expected_str: str):
46+
"""Test the regex works properly for the title from name."""
47+
result = ErrorHandler.get_title_from_name(exception_or_str)
48+
assert expected_str == result
49+
50+
51+
@pytest.mark.parametrize(
52+
["error", "title", "description"],
53+
[
54+
[
55+
commands.UserInputError("some interesting information."),
56+
"User Input Error",
57+
"some interesting information.",
58+
],
59+
[
60+
commands.MissingRequiredArgument(inspect.Parameter("SomethingSpecial", kind=1)),
61+
"Missing Required Argument",
62+
"SomethingSpecial is a required argument that is missing.",
63+
],
64+
[
65+
commands.GuildNotFound("ImportantGuild"),
66+
"Guild Not Found",
67+
'Guild "ImportantGuild" not found.',
68+
],
69+
],
70+
)
71+
@pytest.mark.asyncio
72+
async def test_handle_user_input_error(
73+
cog: ErrorHandler, ctx: mocks.MockContext, error: commands.UserInputError, title: str, description: str
74+
):
75+
"""Test user input errors are handled properly. Does not test with BadUnionArgument."""
76+
embed = await cog.handle_user_input_error(ctx=ctx, error=error, reset_cooldown=False)
77+
78+
assert title == embed.title
79+
assert description == embed.description
80+
81+
82+
@pytest.mark.asyncio
83+
async def test_handle_bot_missing_perms(cog: ErrorHandler):
84+
"""
85+
86+
Test error_handler.handle_bot_missing_perms.
87+
88+
There are some cases here where the bot is unable to send messages, and that should be clear.
89+
"""
90+
...
91+
92+
93+
@pytest.mark.asyncio
94+
async def test_handle_check_failure(cog: ErrorHandler):
95+
"""
96+
Test check failures.
97+
98+
In some cases, this method should result in calling a bot_missing_perms method
99+
because the bot cannot send messages.
100+
"""
101+
...
102+
103+
104+
@pytest.mark.asyncio
105+
async def test_on_command_error(cog: ErrorHandler):
106+
"""Test the general command error method."""
107+
...
108+
109+
110+
class TestErrorHandler:
111+
"""
112+
Test class for the error handler. The problem here is a lot of the errors need to be raised.
113+
114+
Thankfully, most of them do not have extra attributes that we use, and can be easily faked.
115+
"""
116+
117+
errors = {
118+
commands.CommandError: [
119+
commands.ConversionError,
120+
{
121+
commands.UserInputError: [
122+
commands.MissingRequiredArgument,
123+
commands.TooManyArguments,
124+
{
125+
commands.BadArgument: [
126+
commands.MessageNotFound,
127+
commands.MemberNotFound,
128+
commands.GuildNotFound,
129+
commands.UserNotFound,
130+
commands.ChannelNotFound,
131+
commands.ChannelNotReadable,
132+
commands.BadColourArgument,
133+
commands.RoleNotFound,
134+
commands.BadInviteArgument,
135+
commands.EmojiNotFound,
136+
commands.GuildStickerNotFound,
137+
commands.PartialEmojiConversionFailure,
138+
commands.BadBoolArgument,
139+
commands.ThreadNotFound,
140+
]
141+
},
142+
commands.BadUnionArgument,
143+
commands.BadLiteralArgument,
144+
{
145+
commands.ArgumentParsingError: [
146+
commands.UnexpectedQuoteError,
147+
commands.InvalidEndOfQuotedStringError,
148+
commands.ExpectedClosingQuoteError,
149+
]
150+
},
151+
]
152+
},
153+
commands.CommandNotFound,
154+
{
155+
commands.CheckFailure: [
156+
commands.CheckAnyFailure,
157+
commands.PrivateMessageOnly,
158+
commands.NoPrivateMessage,
159+
commands.NotOwner,
160+
commands.MissingPermissions,
161+
commands.BotMissingPermissions,
162+
commands.MissingRole,
163+
commands.BotMissingRole,
164+
commands.MissingAnyRole,
165+
commands.BotMissingAnyRole,
166+
commands.NSFWChannelRequired,
167+
]
168+
},
169+
commands.DisabledCommand,
170+
commands.CommandInvokeError,
171+
commands.CommandOnCooldown,
172+
commands.MaxConcurrencyReached,
173+
]
174+
}

tests/modmail/utils/test_time.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import arrow
2+
import pytest
3+
4+
from modmail.utils import time as utils_time
5+
from modmail.utils.time import TimeStampEnum
6+
7+
8+
@pytest.mark.parametrize(
9+
["timestamp", "expected", "mode"],
10+
[
11+
[arrow.get(1634593650), "<t:1634593650:f>", TimeStampEnum.SHORT_DATE_TIME],
12+
[arrow.get(1), "<t:1:f>", TimeStampEnum.DEFAULT],
13+
[arrow.get(12356941), "<t:12356941:R>", TimeStampEnum.RELATIVE_TIME],
14+
[arrow.get(8675309).datetime, "<t:8675309:D>", TimeStampEnum.LONG_DATE],
15+
],
16+
)
17+
def test_timestamp(timestamp, expected: str, mode: utils_time.TimeStampEnum):
18+
"""Test the timestamp is of the proper form."""
19+
fmtted_timestamp = utils_time.get_discord_formatted_timestamp(timestamp, mode)
20+
assert expected == fmtted_timestamp
21+
22+
23+
def test_enum_default():
24+
"""Ensure that the default mode is of the correct mode, and works properly."""
25+
assert TimeStampEnum.DEFAULT.name == TimeStampEnum.SHORT_DATE_TIME.name
26+
assert TimeStampEnum.DEFAULT.value == TimeStampEnum.SHORT_DATE_TIME.value

0 commit comments

Comments
 (0)