Skip to content

Commit

Permalink
Merge branch 'task/emoji-parsing-helpers' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Nekokatt committed Oct 2, 2020
2 parents 5c4627b + b57d14f commit 9e5121d
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 44 deletions.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ To push branches directly to the remote, you will have to name them like this:
We have nox to help out with running pipelines locally and provides some helpful functionality.

Nox is similar to tox, but uses a pure Python configuration instead of an
INI based configuration. Nox and tox are both tools for generating virtual
INI based configuration. Nox and tox are both tools for generating virtual
environments and running commands in those environments. Examples of usage
include installing, configuring, and running flake8; running py.test, et
include installing, configuring, and running flake8; running py.test, et
cetera.

You can check all the available nox commands by running `nox -l`.
Expand Down
17 changes: 5 additions & 12 deletions examples/image_resources/image_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def on_message(event: hikari.GuildMessageCreateEvent) -> None:
# Do not respond to bots, webhooks, or messages without content or without a prefix.
return

command, _, args = event.content[1:].partition(" ")
command, args = event.content[1:].split(" ", 1)

if command == "image":
await inspect_image(event, args.lstrip())
Expand All @@ -56,17 +56,10 @@ async def inspect_image(event: hikari.GuildMessageCreateEvent, what: str) -> Non
elif what.casefold() in ("guild", "server", "here", "this"):
await event.message.reply("Guild icon", attachment=event.guild.icon_url)

# Show the image for the given custom emoji:
elif custom_emoji_match := re.match(r"<a?:([^:]+):(\d+)>", what):
name, emoji_id = custom_emoji_match.group(1), hikari.Snowflake(custom_emoji_match.group(2))
emoji = bot.cache.get_emoji(emoji_id)
await event.message.reply(f"Emoji {name}", attachment=emoji)

# If any content exists, try treating it as a unicode emoji; only upload if it is actually valid:
elif what.strip():
# If this is not a valid emoji, this will raise hikari.NotFoundError
emoji = hikari.UnicodeEmoji.from_emoji(what)
await event.message.reply("Unicode Emoji", attachment=emoji)
# Show the image for the given emoji if there is some content present.
elif what:
emoji = hikari.Emoji.parse(what)
await event.message.reply(emoji.name, attachment=emoji)

# If nothing was given, we should just return the avatar of the person who ran the command:
else:
Expand Down
80 changes: 68 additions & 12 deletions hikari/emojis.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
__all__: typing.List[str] = ["Emoji", "UnicodeEmoji", "CustomEmoji", "KnownCustomEmoji", "Emojiish"]

import abc
import re
import typing
import unicodedata

Expand All @@ -42,6 +43,7 @@
from hikari import users

_TWEMOJI_PNG_BASE_URL: typing.Final[str] = "https://github.com/twitter/twemoji/raw/master/assets/72x72/"
_CUSTOM_EMOJI_REGEX: typing.Final[re.Pattern[str]] = re.compile(r"<(?P<flags>[^:]*):(?P<name>[^:]*):(?P<id>\d+)>")


@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
Expand Down Expand Up @@ -79,6 +81,30 @@ def is_mentionable(self) -> bool:
def mention(self) -> str:
"""Mention string to use to mention the emoji with."""

@classmethod
def parse(cls, string: str, /) -> Emoji:
"""Parse a given string into an emoji object.
Parameters
----------
string : builtins.str
The emoji object to parse.
Returns
-------
Emoji
The parsed emoji object. This will be a `CustomEmoji` if a custom
emoji ID or mention, or a `UnicodeEmoji` otherwise.
Raises
------
builtins.ValueError
If a mention is given that has an invalid format.
"""
if string.isdigit() or string.startswith("<") and string.endswith(">"):
return CustomEmoji.parse(string)
return UnicodeEmoji.parse(string)


@attr_extensions.with_copy
@attr.s(hash=True, init=True, kw_only=False, slots=True, eq=False, weakref_slot=False)
Expand Down Expand Up @@ -183,21 +209,37 @@ def unicode_escape(self) -> str:

@classmethod
@typing.final
def from_codepoints(cls, codepoint: int, *codepoints: int) -> UnicodeEmoji:
def parse_codepoints(cls, codepoint: int, *codepoints: int) -> UnicodeEmoji:
"""Create a unicode emoji from one or more UTF-32 codepoints."""
return cls(name="".join(map(chr, (codepoint, *codepoints))))

@classmethod
@typing.final
def from_emoji(cls, emoji: str) -> UnicodeEmoji:
"""Create a unicode emoji from a raw emoji."""
return cls(name=emoji)
def parse_unicode_escape(cls, escape: str) -> UnicodeEmoji:
"""Create a unicode emoji from a unicode escape string."""
return cls(name=str(escape.encode("utf-8"), "unicode_escape"))

@classmethod
@typing.final
def from_unicode_escape(cls, escape: str) -> UnicodeEmoji:
"""Create a unicode emoji from a unicode escape string."""
return cls(name=str(escape.encode("utf-8"), "unicode_escape"))
def parse(cls, string: str, /) -> UnicodeEmoji:
"""Parse a given string into a unicode emoji object.
Parameters
----------
string : builtins.str
The emoji object to parse.
Returns
-------
UnicodeEmoji
The parsed UnicodeEmoji object.
"""

# Ensure validity.
for i, codepoint in enumerate(string, start=1):
unicodedata.name(codepoint)

return cls(name=string)


@attr_extensions.with_copy
Expand Down Expand Up @@ -226,11 +268,6 @@ class CustomEmoji(snowflakes.Unique, Emoji):
https://github.com/discord/discord-api-docs/issues/1614#issuecomment-628548913
"""

app: traits.RESTAware = attr.ib(
repr=False, eq=False, hash=False, init=True, metadata={attr_extensions.SKIP_DEEP_COPY: True}
)
"""The client application that models may use for procedures."""

id: snowflakes.Snowflake = attr.ib(eq=True, hash=True, repr=True)
"""The ID of this entity."""

Expand Down Expand Up @@ -273,6 +310,20 @@ def url(self) -> str:

return routes.CDN_CUSTOM_EMOJI.compile(urls.CDN_URL, emoji_id=self.id, file_format=ext)

@classmethod
def parse(cls, string: str, /) -> CustomEmoji:
if string.isdigit():
return CustomEmoji(id=snowflakes.Snowflake(string), name=None, is_animated=None)

if emoji_match := _CUSTOM_EMOJI_REGEX.match(string):
return CustomEmoji(
id=snowflakes.Snowflake(emoji_match.group("id")),
name=emoji_match.group("name"),
is_animated=emoji_match.group("flags").lower() == "a",
)

raise ValueError("Expected an emoji ID or emoji mention")


@attr.s(eq=True, hash=True, init=True, kw_only=True, slots=True, weakref_slot=False)
class KnownCustomEmoji(CustomEmoji):
Expand All @@ -282,6 +333,11 @@ class KnownCustomEmoji(CustomEmoji):
_are_ part of. As a result, it contains a lot more information with it.
"""

app: traits.RESTAware = attr.ib(
repr=False, eq=False, hash=False, init=True, metadata={attr_extensions.SKIP_DEEP_COPY: True}
)
"""The client application that models may use for procedures."""

guild_id: snowflakes.Snowflake = attr.ib(eq=False, hash=False, repr=False)
"""The ID of the guild this emoji belongs to."""

Expand Down
1 change: 0 additions & 1 deletion hikari/impl/entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,6 @@ def deserialize_unicode_emoji(self, payload: data_binding.JSONObject) -> emoji_m

def deserialize_custom_emoji(self, payload: data_binding.JSONObject) -> emoji_models.CustomEmoji:
return emoji_models.CustomEmoji(
app=self._app,
id=snowflakes.Snowflake(payload["id"]),
name=payload["name"],
is_animated=payload.get("animated", False),
Expand Down
2 changes: 1 addition & 1 deletion scripts/test_twemoji_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
for i, emoji in enumerate(mapping, start=1):
emoji_surrogates = emoji["surrogates"]
name = emoji["primaryName"]
emoji = emojis.UnicodeEmoji.from_emoji(emoji_surrogates)
emoji = emojis.UnicodeEmoji.parse(emoji_surrogates)

if emoji.filename in known_files:
valid_emojis.append((emoji_surrogates, name))
Expand Down
3 changes: 1 addition & 2 deletions tests/hikari/impl/test_entity_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,8 +1303,7 @@ def custom_emoji_payload(self):

def test_deserialize_custom_emoji(self, entity_factory_impl, mock_app, custom_emoji_payload):
emoji = entity_factory_impl.deserialize_custom_emoji(custom_emoji_payload)
assert emoji.app is mock_app
assert emoji.id == 691225175349395456
assert emoji.id == snowflakes.Snowflake(691225175349395456)
assert emoji.name == "test"
assert emoji.is_animated is True
assert isinstance(emoji, emoji_models.CustomEmoji)
Expand Down
2 changes: 1 addition & 1 deletion tests/hikari/impl/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ def test__generate_allowed_mentions(self, rest_client, function_input, expected_
@pytest.mark.parametrize( # noqa: PT014 - Duplicate test cases (false positive)
("emoji", "expected_return"),
[
(emojis.CustomEmoji(id=123, name="rooYay", app=object(), is_animated=False), "rooYay:123"),
(emojis.CustomEmoji(id=123, name="rooYay", is_animated=False), "rooYay:123"),
("👌", "👌"),
("\N{OK HAND SIGN}", "\N{OK HAND SIGN}"),
("<:rooYay:123>", "rooYay:123"),
Expand Down
76 changes: 63 additions & 13 deletions tests/hikari/test_emojis.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,74 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import mock
import pytest

from hikari import emojis
from hikari import snowflakes


def test_UnicodeEmoji_str_operator():
mock_emoji = mock.Mock(emojis.UnicodeEmoji)
mock_emoji.name = "\N{OK HAND SIGN}"
assert emojis.UnicodeEmoji.__str__(mock_emoji) == "\N{OK HAND SIGN}"
class TestEmoji:
@pytest.mark.parametrize(
("input", "output"),
[
("12345", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name=None, is_animated=None)),
("<:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=False)),
("<bar:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=False)),
("<a:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=True)),
("\N{OK HAND SIGN}", emojis.UnicodeEmoji(name="\N{OK HAND SIGN}")),
(
"\N{REGIONAL INDICATOR SYMBOL LETTER G}\N{REGIONAL INDICATOR SYMBOL LETTER B}",
emojis.UnicodeEmoji(
name="\N{REGIONAL INDICATOR SYMBOL LETTER G}\N{REGIONAL INDICATOR SYMBOL LETTER B}"
),
),
],
)
def test_parse(self, input, output):
assert emojis.Emoji.parse(input) == output


def test_CustomEmoji_str_operator():
mock_emoji = mock.Mock(emojis.CustomEmoji, emojis.CustomEmoji)
mock_emoji.name = "peepoSad"
assert emojis.CustomEmoji.__str__(mock_emoji) == "peepoSad"
class TestUnicodeEmoji:
def test_str_operator(self):
assert emojis.UnicodeEmoji(name="\N{OK HAND SIGN}") == "\N{OK HAND SIGN}"

@pytest.mark.parametrize(
("input", "output"),
[
("\N{OK HAND SIGN}", emojis.UnicodeEmoji(name="\N{OK HAND SIGN}")),
(
"\N{REGIONAL INDICATOR SYMBOL LETTER G}\N{REGIONAL INDICATOR SYMBOL LETTER B}",
emojis.UnicodeEmoji(
name="\N{REGIONAL INDICATOR SYMBOL LETTER G}\N{REGIONAL INDICATOR SYMBOL LETTER B}"
),
),
],
)
def test_parse(self, input, output):
assert emojis.UnicodeEmoji.parse(input) == output

def test_CustomEmoji_str_operator_when_name_is_None():
mock_emoji = mock.Mock(emojis.CustomEmoji, emojis.CustomEmoji, id=42069)
mock_emoji.name = None
assert emojis.CustomEmoji.__str__(mock_emoji) == "Unnamed emoji ID 42069"

class TestCustomEmoji:
def test_str_operator_when_populated_name(self):
emoji = emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="peepoSad", is_animated=True)
assert str(emoji) == "peepoSad"

def test_str_operator_when_name_is_None(self):
emoji = emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name=None, is_animated=True)
assert str(emoji) == "Unnamed emoji ID 12345"

@pytest.mark.parametrize(
("input", "output"),
[
("12345", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name=None, is_animated=None)),
("<:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=False)),
("<bar:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=False)),
("<a:foo:12345>", emojis.CustomEmoji(id=snowflakes.Snowflake(12345), name="foo", is_animated=True)),
],
)
def test_parse(self, input, output):
assert emojis.CustomEmoji.parse(input) == output

def test_parse_unhappy_path(self):
with pytest.raises(ValueError, match="Expected an emoji ID or emoji mention"):
emojis.CustomEmoji.parse("xxx")

0 comments on commit 9e5121d

Please sign in to comment.