#!/usr/bin/env python3
#
# fields.py
"""
Primitive field types.
"""
#
# Copyright © 2023 Dominic Davis-Foster <dominic@davis-foster.co.uk>
#
# 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.
#
# stdlib
from typing import Any, Generic, Type, TypeVar
# 3rd party
import attrs # nodep
__all__ = (
"Boolean",
"FieldType",
"Integer",
"Number",
"String",
)
_FT = TypeVar("_FT")
[docs]class FieldType(Generic[_FT]):
"""
Customisable config field type.
"""
#: The Python type of the object.
field_type: Type[_FT]
#: String name of the field.
field_name: str
def __new__(cls, default: _FT) -> _FT: # type: ignore[misc]
"""
The wrong way to construct an attrs field.
:param default:
"""
# TODO: emit warning
return cls.field(default=default)
[docs] @classmethod
def validator( # noqa: PRM002
cls: Type["FieldType"],
inst: object,
attr: attrs.Attribute,
value: Any,
) -> None:
"""
Check if a value conforms to this field's expected datatype.
This method is called by attrs.
"""
if not isinstance(value, cls.field_type): # pragma: no cover
err_msg = f"'{attr.name}' must be {cls.field_name} (got {value!r} that is a {value.__class__!r})."
raise TypeError(err_msg)
[docs] @classmethod
def on_setattr( # noqa: PRM002
cls: Type["FieldType"],
inst: object,
attr: attrs.Attribute,
value: Any,
) -> None:
"""
Converts values on ``__setattr__``.
This method is called by attrs.
"""
return cls.field_type(value)
[docs] @classmethod
def field(cls: Type["FieldType"], default: _FT) -> _FT:
"""
Construct an attrs field.
:param default:
"""
# Actually returns attr.Attribute, but mypy doesn't like it
return attrs.field(
default=default,
converter=cls.field_type,
validator=cls.validator,
on_setattr=cls.on_setattr,
)
# TODO: sequence types
[docs]class Boolean(FieldType):
"""
Boolean config field type.
"""
field_type = bool
field_name = "boolean"
[docs]class String(FieldType):
"""
String config field type.
"""
field_type = str
field_name = "a string"
[docs]class Integer(FieldType):
"""
Integer config field type.
"""
field_type = int
field_name = "an integer"
[docs]class Number(FieldType):
"""
Numerical config field type.
"""
field_type = float
field_name = "a number"
[docs] @classmethod
def validator( # noqa: PRM002
cls: Type["FieldType"],
inst: object,
attr: attrs.Attribute,
value: Any,
) -> None:
"""
Check if a value is a number.
This method is called by attrs.
"""
if not isinstance(value, (int, float)): # pragma: no cover
err_msg = f"'{attr.name}' must be {cls.field_name} (got {value!r} that is a {value.__class__!r})."
raise TypeError(err_msg)