Source code for fastpyxl.descriptors.base

# Copyright (c) 2010-2024 fastpyxl

"""
Based on Python Cookbook 3rd Edition, 8.13
http://chimera.labs.oreilly.com/books/1230000000393/ch08.html#_discussiuncion_130
"""

import datetime
import re
from typing import Any, cast

from fastpyxl import DEBUG
from fastpyxl.utils.datetime import from_ISO8601


[docs] class Descriptor: namespace = None nested = False def __init__(self, name=None, **kw): self.name = name for k, v in kw.items(): setattr(self, k, v) def __set__(self, instance, value): instance.__dict__[self.name] = value
[docs] class Typed(Descriptor): """Values must of a particular type""" expected_type = type(None) allow_none = False nested = False def __init__(self, *args, **kw): super().__init__(*args, **kw) self.__doc__ = f"Values must be of type {self.expected_type}" def __set__(self, instance, value): if not isinstance(value, self.expected_type): if (not self.allow_none or (self.allow_none and value is not None)): msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but value is {type(value)}" if DEBUG: msg = f"{instance.__class__}.{self.name} should be {self.expected_type} but {value} is {type(value)}" raise TypeError(msg) super().__set__(instance, value) def __repr__(self): return self.__doc__
def _convert(expected_type, value): """ Check value is of or can be converted to expected type. """ if not isinstance(value, expected_type): try: value = expected_type(value) except (ValueError, TypeError): raise TypeError("expected " + str(expected_type)) return value
[docs] class Convertible(Typed): """Values must be convertible to a particular type""" def __set__(self, instance, value): if ((self.allow_none and value is not None) or not self.allow_none): value = _convert(self.expected_type, value) super().__set__(instance, value)
[docs] class Max(Convertible): """Values must be less than a `max` value""" expected_type = float allow_none = False def __init__(self, **kw): if "max" not in kw and not hasattr(self, "max"): raise TypeError("missing max value") super().__init__(**kw) def __set__(self, instance, value): s = cast(Any, self) if ((self.allow_none and value is not None) or not self.allow_none): value = _convert(self.expected_type, value) if value > s.max: raise ValueError("Max value is {0}".format(s.max)) super().__set__(instance, value)
[docs] class Min(Convertible): """Values must be greater than a `min` value""" expected_type = float allow_none = False def __init__(self, **kw): if "min" not in kw and not hasattr(self, "min"): raise TypeError("missing min value") super().__init__(**kw) def __set__(self, instance, value): s = cast(Any, self) if ((self.allow_none and value is not None) or not self.allow_none): value = _convert(self.expected_type, value) if value < s.min: raise ValueError("Min value is {0}".format(s.min)) super().__set__(instance, value)
[docs] class MinMax(Min, Max): """Values must be greater than `min` value and less than a `max` one"""
[docs] class Set(Descriptor): """Value can only be from a set of know values""" def __init__(self, name=None, **kw): if "values" not in kw: raise TypeError("missing set of values") kw["values"] = set(kw["values"]) super().__init__(name, **kw) self.__doc__ = "Value must be one of {0}".format(cast(Any, self).values) def __set__(self, instance, value): if value not in cast(Any, self).values: raise ValueError(self.__doc__) super().__set__(instance, value)
[docs] class NoneSet(Set): """'none' will be treated as None""" def __init__(self, name=None, **kw): super().__init__(name, **kw) cast(Any, self).values.add(None) def __set__(self, instance, value): if value == "none": value = None super().__set__(instance, value)
[docs] class Integer(Convertible): expected_type = int
[docs] class Float(Convertible): expected_type = float
[docs] class Bool(Convertible): expected_type = bool def __set__(self, instance, value): if isinstance(value, str): if value in ("false", "f", "0"): value = False super().__set__(instance, value)
[docs] class String(Typed): expected_type = str
[docs] class Text(String, Convertible): pass
[docs] class ASCII(Typed): expected_type = bytes
[docs] class Tuple(Typed): expected_type = tuple
[docs] class Length(Descriptor): def __init__(self, name=None, **kw): if "length" not in kw: raise TypeError("value length must be supplied") super().__init__(**kw) def __set__(self, instance, value): s = cast(Any, self) if len(value) != s.length: raise ValueError("Value must be length {0}".format(s.length)) super().__set__(instance, value)
[docs] class Default(Typed): """ When called returns an instance of the expected type. Additional default values can be passed in to the descriptor """ def __init__(self, name=None, **kw): if "defaults" not in kw: kw["defaults"] = {} super().__init__(**kw) def __call__(self): return self.expected_type()
[docs] class Alias(Descriptor): """ Aliases can be used when either the desired attribute name is not allowed or confusing in Python (eg. "type") or a more descriptive name is desired (eg. "underline" for "u") """ def __init__(self, alias): self.alias = alias def __set__(self, instance, value): setattr(instance, self.alias, value) def __get__(self, instance, cls): return getattr(instance, self.alias)
[docs] class MatchPattern(Descriptor): """Values must match a regex pattern """ allow_none = False def __init__(self, name=None, **kw): if "pattern" not in kw and not hasattr(self, "pattern"): raise TypeError("missing pattern value") super().__init__(name, **kw) s = cast(Any, self) s.test_pattern = re.compile(s.pattern, re.VERBOSE) def __set__(self, instance, value): s = cast(Any, self) if value is None and not self.allow_none: raise ValueError("Value must not be none") if ((self.allow_none and value is not None) or not self.allow_none): if not s.test_pattern.match(value): raise ValueError("Value does not match pattern {0}".format(s.pattern)) super().__set__(instance, value)
[docs] class DateTime(Typed): expected_type = datetime.datetime def __set__(self, instance, value): if value is not None and isinstance(value, str): try: value = from_ISO8601(value) except ValueError: raise ValueError("Value must be ISO datetime format") super().__set__(instance, value)