mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-04-18 23:15:48 +00:00
Add Colour.from_str factory method
This moves the command extension parsing code over to the main library since it can be potentially useful for others.
This commit is contained in:
parent
84e6b9283d
commit
de941ababe
@ -25,6 +25,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import colorsys
|
import colorsys
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
||||||
|
|
||||||
@ -36,6 +37,44 @@ __all__ = (
|
|||||||
'Color',
|
'Color',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9.]+%?)\s*,\s*(?P<g>[0-9.]+%?)\s*,\s*(?P<b>[0-9.]+%?)\s*\)')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_hex_number(argument: str) -> Colour:
|
||||||
|
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
|
||||||
|
try:
|
||||||
|
value = int(arg, base=16)
|
||||||
|
if not (0 <= value <= 0xFFFFFF):
|
||||||
|
raise ValueError('hex number out of range for 24-bit colour')
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('invalid hex digit given') from None
|
||||||
|
else:
|
||||||
|
return Color(value=value)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rgb_number(number: str) -> int:
|
||||||
|
if number[-1] == '%':
|
||||||
|
value = float(number[:-1])
|
||||||
|
if not (0 <= value <= 100):
|
||||||
|
raise ValueError('rgb percentage can only be between 0 to 100')
|
||||||
|
return round(255 * (value / 100))
|
||||||
|
|
||||||
|
value = int(number)
|
||||||
|
if not (0 <= value <= 255):
|
||||||
|
raise ValueError('rgb number can only be between 0 to 255')
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rgb(argument: str, *, regex: re.Pattern[str] = RGB_REGEX) -> Colour:
|
||||||
|
match = regex.match(argument)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError('invalid rgb syntax found')
|
||||||
|
|
||||||
|
red = parse_rgb_number(match.group('r'))
|
||||||
|
green = parse_rgb_number(match.group('g'))
|
||||||
|
blue = parse_rgb_number(match.group('b'))
|
||||||
|
return Color.from_rgb(red, green, blue)
|
||||||
|
|
||||||
|
|
||||||
class Colour:
|
class Colour:
|
||||||
"""Represents a Discord role colour. This class is similar
|
"""Represents a Discord role colour. This class is similar
|
||||||
@ -130,6 +169,44 @@ class Colour:
|
|||||||
rgb = colorsys.hsv_to_rgb(h, s, v)
|
rgb = colorsys.hsv_to_rgb(h, s, v)
|
||||||
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
return cls.from_rgb(*(int(x * 255) for x in rgb))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_str(cls, value: str) -> Self:
|
||||||
|
"""Constructs a :class:`Colour` from a string.
|
||||||
|
|
||||||
|
The following formats are accepted:
|
||||||
|
|
||||||
|
- ``0x<hex>``
|
||||||
|
- ``#<hex>``
|
||||||
|
- ``0x#<hex>``
|
||||||
|
- ``rgb(<number>, <number>, <number>)``
|
||||||
|
|
||||||
|
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
|
||||||
|
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
ValueError
|
||||||
|
The string could not be converted into a colour.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if value[0] == '#':
|
||||||
|
return parse_hex_number(value[1:])
|
||||||
|
|
||||||
|
if value[0:2] == '0x':
|
||||||
|
rest = value[2:]
|
||||||
|
# Legacy backwards compatible syntax
|
||||||
|
if rest.startswith('#'):
|
||||||
|
return parse_hex_number(rest[1:])
|
||||||
|
return parse_hex_number(rest)
|
||||||
|
|
||||||
|
arg = value.lower()
|
||||||
|
if arg[0:3] == 'rgb':
|
||||||
|
return parse_rgb(arg)
|
||||||
|
|
||||||
|
raise ValueError('unknown colour format given')
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default(cls) -> Self:
|
def default(cls) -> Self:
|
||||||
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
"""A factory method that returns a :class:`Colour` with a value of ``0``."""
|
||||||
|
@ -627,57 +627,11 @@ class ColourConverter(Converter[discord.Colour]):
|
|||||||
Added support for ``rgb`` function and 3-digit hex shortcuts
|
Added support for ``rgb`` function and 3-digit hex shortcuts
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)')
|
|
||||||
|
|
||||||
def parse_hex_number(self, argument: str) -> discord.Colour:
|
|
||||||
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
|
|
||||||
try:
|
|
||||||
value = int(arg, base=16)
|
|
||||||
if not (0 <= value <= 0xFFFFFF):
|
|
||||||
raise BadColourArgument(argument)
|
|
||||||
except ValueError:
|
|
||||||
raise BadColourArgument(argument)
|
|
||||||
else:
|
|
||||||
return discord.Color(value=value)
|
|
||||||
|
|
||||||
def parse_rgb_number(self, argument: str, number: str) -> int:
|
|
||||||
if number[-1] == '%':
|
|
||||||
value = int(number[:-1])
|
|
||||||
if not (0 <= value <= 100):
|
|
||||||
raise BadColourArgument(argument)
|
|
||||||
return round(255 * (value / 100))
|
|
||||||
|
|
||||||
value = int(number)
|
|
||||||
if not (0 <= value <= 255):
|
|
||||||
raise BadColourArgument(argument)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def parse_rgb(self, argument: str, *, regex: re.Pattern[str] = RGB_REGEX) -> discord.Colour:
|
|
||||||
match = regex.match(argument)
|
|
||||||
if match is None:
|
|
||||||
raise BadColourArgument(argument)
|
|
||||||
|
|
||||||
red = self.parse_rgb_number(argument, match.group('r'))
|
|
||||||
green = self.parse_rgb_number(argument, match.group('g'))
|
|
||||||
blue = self.parse_rgb_number(argument, match.group('b'))
|
|
||||||
return discord.Color.from_rgb(red, green, blue)
|
|
||||||
|
|
||||||
async def convert(self, ctx: Context[BotT], argument: str) -> discord.Colour:
|
async def convert(self, ctx: Context[BotT], argument: str) -> discord.Colour:
|
||||||
if argument[0] == '#':
|
try:
|
||||||
return self.parse_hex_number(argument[1:])
|
return discord.Colour.from_str(argument)
|
||||||
|
except ValueError:
|
||||||
if argument[0:2] == '0x':
|
arg = argument.lower().replace(' ', '_')
|
||||||
rest = argument[2:]
|
|
||||||
# Legacy backwards compatible syntax
|
|
||||||
if rest.startswith('#'):
|
|
||||||
return self.parse_hex_number(rest[1:])
|
|
||||||
return self.parse_hex_number(rest)
|
|
||||||
|
|
||||||
arg = argument.lower()
|
|
||||||
if arg[0:3] == 'rgb':
|
|
||||||
return self.parse_rgb(arg)
|
|
||||||
|
|
||||||
arg = arg.replace(' ', '_')
|
|
||||||
method = getattr(discord.Colour, arg, None)
|
method = getattr(discord.Colour, arg, None)
|
||||||
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
|
||||||
raise BadColourArgument(arg)
|
raise BadColourArgument(arg)
|
||||||
|
69
tests/test_colour.py
Normal file
69
tests/test_colour.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
"""
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015-present Rapptz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of this software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation
|
||||||
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||||
|
and/or sell copies of the Software, and to permit persons to whom the
|
||||||
|
Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import discord
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('value', 'expected'),
|
||||||
|
[
|
||||||
|
('0xFF1294', 0xFF1294),
|
||||||
|
('0xff1294', 0xFF1294),
|
||||||
|
('0xFFF', 0xFFFFFF),
|
||||||
|
('0xfff', 0xFFFFFF),
|
||||||
|
('#abcdef', 0xABCDEF),
|
||||||
|
('#ABCDEF', 0xABCDEF),
|
||||||
|
('#ABC', 0xAABBCC),
|
||||||
|
('#abc', 0xAABBCC),
|
||||||
|
('rgb(68,36,59)', 0x44243B),
|
||||||
|
('rgb(26.7%, 14.1%, 23.1%)', 0x44243B),
|
||||||
|
('rgb(20%, 24%, 56%)', 0x333D8F),
|
||||||
|
('rgb(20%, 23.9%, 56.1%)', 0x333D8F),
|
||||||
|
('rgb(51, 61, 143)', 0x333D8F),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_from_str(value, expected):
|
||||||
|
assert discord.Colour.from_str(value) == discord.Colour(expected)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
('value'),
|
||||||
|
[
|
||||||
|
'not valid',
|
||||||
|
'0xYEAH',
|
||||||
|
'#YEAH',
|
||||||
|
'#yeah',
|
||||||
|
'yellow',
|
||||||
|
'rgb(-10, -20, -30)',
|
||||||
|
'rgb(30, -1, 60)',
|
||||||
|
'invalid(a, b, c)',
|
||||||
|
'rgb(',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_from_str_failures(value):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
discord.Colour.from_str(value)
|
Loading…
x
Reference in New Issue
Block a user