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:
Rapptz
2022-04-20 06:49:28 -04:00
parent 84e6b9283d
commit de941ababe
3 changed files with 154 additions and 54 deletions

View File

@@ -25,6 +25,7 @@ from __future__ import annotations
import colorsys
import random
import re
from typing import TYPE_CHECKING, Optional, Tuple, Union
@@ -36,6 +37,44 @@ __all__ = (
'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:
"""Represents a Discord role colour. This class is similar
@@ -130,6 +169,44 @@ class Colour:
rgb = colorsys.hsv_to_rgb(h, s, v)
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
def default(cls) -> Self:
"""A factory method that returns a :class:`Colour` with a value of ``0``."""

View File

@@ -627,61 +627,15 @@ class ColourConverter(Converter[discord.Colour]):
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:
if argument[0] == '#':
return self.parse_hex_number(argument[1:])
if argument[0:2] == '0x':
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)
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
try:
return discord.Colour.from_str(argument)
except ValueError:
arg = argument.lower().replace(' ', '_')
method = getattr(discord.Colour, arg, None)
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
ColorConverter = ColourConverter