Skip to content
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

add missing countries #32

Merged
merged 6 commits into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ repos:
entry: pyupgrade --py37-plus
types: [python]
language: system
exclude: "pydantic_extra_types/types/phone_numbers.py"
180 changes: 145 additions & 35 deletions pydantic_extra_types/types/color.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""
Color definitions are used as per CSS3 specification:
http://www.w3.org/TR/css3-color/#svg-color
Color definitions are used as per the CSS3
[CSS Color Module Level 3](http://www.w3.org/TR/css3-color/#svg-color) specification.

A few colors have multiple names referring to the sames colors, eg. `grey` and `gray` or `aqua` and `cyan`.

In these cases the LAST color when sorted alphabetically takes preferences,
eg. Color((0, 255, 255)).as_named() == 'cyan' because "cyan" comes after "aqua".
In these cases the _last_ color when sorted alphabetically takes preferences,
eg. `Color((0, 255, 255)).as_named() == 'cyan'` because "cyan" comes after "aqua".
"""
import math
import re
from colorsys import hls_to_rgb, rgb_to_hls
from typing import Any, Dict, Optional, Tuple, Union, cast
from typing import Any, Callable, Optional, Tuple, Type, Union, cast

from pydantic_core import PydanticCustomError, core_schema
from pydantic_core import CoreSchema, PydanticCustomError, core_schema

from pydantic._internal import _repr, _utils
from pydantic._internal._schema_generation_shared import GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue

ColorTuple = Union[Tuple[int, int, int], Tuple[int, int, int, float]]
ColorType = Union[ColorTuple, str]
Expand Down Expand Up @@ -63,6 +65,10 @@ def __getitem__(self, item: Any) -> Any:


class Color(_repr.Representation):
"""
Represents a color.
"""

__slots__ = '_original', '_rgba'

def __init__(self, value: ColorType) -> None:
Expand All @@ -84,17 +90,34 @@ def __init__(self, value: ColorType) -> None:
self._original = value

@classmethod
def __pydantic_modify_json_schema__(cls, field_schema: Dict[str, Any]) -> Dict[str, Any]:
def __get_pydantic_json_schema__(
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
field_schema = {} # type: ignore
field_schema.update(type='string', format='color')
return field_schema
Comment on lines +96 to 98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should just be return {'type': 'string', 'format': 'color'}


def original(self) -> ColorType:
"""
Original value passed to Color
Original value passed to `Color`.
"""
return self._original

def as_named(self, *, fallback: bool = False) -> str:
"""
Returns the name of the color if it can be found in `COLORS_BY_VALUE` dictionary,
otherwise returns the hexadecimal representation of the color or raises `ValueError`.

Args:
fallback (bool): If True, falls back to returning the hexadecimal representation of
the color instead of raising a ValueError when no named color is found.

Returns:
str: The name of the color, or the hexadecimal representation of the color.

Raises:
ValueError: When no named color is found and fallback is `False`.
"""
if self._rgba.alpha is None:
rgb = cast(Tuple[int, int, int], self.as_rgb_tuple())
try:
Expand All @@ -108,9 +131,13 @@ def as_named(self, *, fallback: bool = False) -> str:
return self.as_hex()

def as_hex(self) -> str:
"""
Hex string representing the color can be 3, 4, 6 or 8 characters depending on whether the string
"""Returns the hexadecimal representation of the color.

Hex string representing the color can be 3, 4, 6, or 8 characters depending on whether the string
a "short" representation of the color is possible and whether there's an alpha channel.

Returns:
str: The hexadecimal representation of the color.
"""
values = [float_to_255(c) for c in self._rgba[:3]]
if self._rgba.alpha is not None:
Expand All @@ -123,7 +150,7 @@ def as_hex(self) -> str:

def as_rgb(self) -> str:
"""
Color as an rgb(<r>, <g>, <b>) or rgba(<r>, <g>, <b>, <a>) string.
Color as an `rgb(<r>, <g>, <b>)` or `rgba(<r>, <g>, <b>, <a>)` string.
"""
if self._rgba.alpha is None:
return f'rgb({float_to_255(self._rgba.r)}, {float_to_255(self._rgba.g)}, {float_to_255(self._rgba.b)})'
Expand All @@ -135,13 +162,17 @@ def as_rgb(self) -> str:

def as_rgb_tuple(self, *, alpha: Optional[bool] = None) -> ColorTuple:
"""
Color as an RGB or RGBA tuple; red, green and blue are in the range 0 to 255, alpha if included is
in the range 0 to 1.
Returns the color as an RGB or RGBA tuple.

Args:
alpha (Optional[bool]): Whether to include the alpha channel. There are three options for this input:
`None` (default): Include alpha only if it's set. (e.g. not `None`)
`True`: Always include alpha.
`False`: Always omit alpha.

:param alpha: whether to include the alpha channel, options are
None - (default) include alpha only if it's set (e.g. not None)
True - always include alpha,
False - always omit alpha,
Returns:
ColorTuple: A tuple that contains the values of the red, green, and blue channels in the range 0 to 255.
If alpha is included, it is in the range 0 to 1.
"""
r, g, b = (float_to_255(c) for c in self._rgba[:3])
if alpha is None:
Expand All @@ -157,7 +188,7 @@ def as_rgb_tuple(self, *, alpha: Optional[bool] = None) -> ColorTuple:

def as_hsl(self) -> str:
"""
Color as an hsl(<h>, <s>, <l>) or hsl(<h>, <s>, <l>, <a>) string.
Color as an `hsl(<h>, <s>, <l>)` or `hsl(<h>, <s>, <l>, <a>)` string.
"""
if self._rgba.alpha is None:
h, s, li = self.as_hsl_tuple(alpha=False) # type: ignore
Expand All @@ -168,15 +199,20 @@ def as_hsl(self) -> str:

def as_hsl_tuple(self, *, alpha: Optional[bool] = None) -> HslColorTuple:
"""
Color as an HSL or HSLA tuple, e.g. hue, saturation, lightness and optionally alpha; all elements are in
the range 0 to 1.
Returns the color as an HSL or HSLA tuple.

Args:
alpha (Optional[bool]): Whether to include the alpha channel.
`None` (default): Include the alpha channel only if it's set (e.g. not `None`).
`True`: Always include alpha.
`False`: Always omit alpha.

NOTE: this is HSL as used in HTML and most other places, not HLS as used in python's colorsys.
Returns:
HslColorTuple: The color as a tuple of hue, saturation, lightness, and alpha (if included).
All elements are in the range 0 to 1.

:param alpha: whether to include the alpha channel, options are
None - (default) include alpha only if it's set (e.g. not None)
True - always include alpha,
False - always omit alpha,
Note:
This is HSL as used in HTML and most other places, not HLS as used in Python's `colorsys`.
"""
h, l, s = rgb_to_hls(self._rgba.r, self._rgba.g, self._rgba.b) # noqa: E741
if alpha is None:
Expand All @@ -194,7 +230,9 @@ def _alpha_float(self) -> float:
return 1 if self._rgba.alpha is None else self._rgba.alpha

@classmethod
def __get_pydantic_core_schema__(cls, **_kwargs: Any) -> core_schema.PlainValidatorFunctionSchema:
def __get_pydantic_core_schema__(
cls, source: Type[Any], handler: Callable[[Any], CoreSchema]
) -> core_schema.CoreSchema:
return core_schema.general_plain_validator_function(
cls._validate, serialization=core_schema.to_string_ser_schema()
)
Expand All @@ -217,8 +255,16 @@ def __hash__(self) -> int:


def parse_tuple(value: Tuple[Any, ...]) -> RGBA:
"""
Parse a tuple or list as a color.
"""Parse a tuple or list to get RGBA values.

Args:
value (Tuple[Any, ...]): A tuple or list.

Returns:
RGBA: An RGBA tuple parsed from the input tuple.

Raises:
PydanticCustomError: If tuple is not valid.
"""
if len(value) == 3:
r, g, b = (parse_color_value(v) for v in value)
Expand All @@ -232,12 +278,24 @@ def parse_tuple(value: Tuple[Any, ...]) -> RGBA:

def parse_str(value: str) -> RGBA:
"""
Parse a string to an RGBA tuple, trying the following formats (in this order):
* named color, see COLORS_BY_NAME below
Parse a string representing a color to an RGBA tuple.

Possible formats for the input string include:

* named color, see `COLORS_BY_NAME`
* hex short eg. `<prefix>fff` (prefix can be `#`, `0x` or nothing)
* hex long eg. `<prefix>ffffff` (prefix can be `#`, `0x` or nothing)
* `rgb(<r>, <g>, <b>) `
* `rgb(<r>, <g>, <b>)`
* `rgba(<r>, <g>, <b>, <a>)`

Args:
value (str): A string representing a color.

Returns:
RGBA: An RGBA tuple parsed from the input string.

Raises:
ValueError: If the input string cannot be parsed to an RGBA tuple.
"""
value_lower = value.lower()
try:
Expand Down Expand Up @@ -279,13 +337,34 @@ def parse_str(value: str) -> RGBA:


def ints_to_rgba(r: Union[int, str], g: Union[int, str], b: Union[int, str], alpha: Optional[float] = None) -> RGBA:
"""
Converts integer or string values for RGB color and an optional alpha value to an `RGBA` object.

Args:
r (Union[int, str]): An integer or string representing the red color value.
g (Union[int, str]): An integer or string representing the green color value.
b (Union[int, str]): An integer or string representing the blue color value.
alpha (float, optional): A float representing the alpha value. Defaults to None.

Returns:
RGBA: An instance of the `RGBA` class with the corresponding color and alpha values.
"""
return RGBA(parse_color_value(r), parse_color_value(g), parse_color_value(b), parse_float_alpha(alpha))


def parse_color_value(value: Union[int, str], max_val: int = 255) -> float:
"""
Parse a value checking it's a valid int in the range 0 to max_val and divide by max_val to give a number
in the range 0 to 1
Parse the color value provided and return a number between 0 and 1.

Args:
value (Union[int, str]): An integer or string color value.
max_val (int, optional): Maximum range value. Defaults to 255.

Raises:
PydanticCustomError: If the value is not a valid color.

Returns:
float: A number between 0 and 1.
"""
try:
color = float(value)
Expand All @@ -303,7 +382,16 @@ def parse_color_value(value: Union[int, str], max_val: int = 255) -> float:

def parse_float_alpha(value: Union[None, str, float, int]) -> Optional[float]:
"""
Parse a value checking it's a valid float in the range 0 to 1
Parse an alpha value checking it's a valid float in the range 0 to 1.

Args:
value (Union[None, str, float, int]): The input value to parse.

Returns:
Optional[float]: The parsed value as a float, or `None` if the value was None or equal 1.

Raises:
PydanticCustomError: If the input value cannot be successfully parsed as a float in the expected range.
"""
if value is None:
return None
Expand All @@ -325,7 +413,17 @@ def parse_float_alpha(value: Union[None, str, float, int]) -> Optional[float]:

def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: Optional[float] = None) -> RGBA:
"""
Parse raw hue, saturation, lightness and alpha values and convert to RGBA.
Parse raw hue, saturation, lightness, and alpha values and convert to RGBA.

Args:
h (str): The hue value.
h_units (str): The unit for hue value.
sat (str): The saturation value.
light (str): The lightness value.
alpha (Optional[float]): Alpha value.

Returns:
RGBA: An instance of `RGBA`.
"""
s_value, l_value = parse_color_value(sat, 100), parse_color_value(light, 100)

Expand All @@ -343,6 +441,18 @@ def parse_hsl(h: str, h_units: str, sat: str, light: str, alpha: Optional[float]


def float_to_255(c: float) -> int:
"""
Converts a float value between 0 and 1 (inclusive) to an integer between 0 and 255 (inclusive).

Args:
c (float): The float value to be converted. Must be between 0 and 1 (inclusive).

Returns:
int: The integer equivalent of the given float value rounded to the nearest whole number.

Raises:
ValueError: If the given float value is outside the acceptable range of 0 to 1 (inclusive).
"""
return int(round(c * 255))


Expand Down
Loading