Source code for shuup.utils.properties

from shuup.core.pricing import Price, TaxfulPrice, TaxlessPrice
from shuup.utils.money import Money
from shuup.utils.numbers import UnitMixupError


[docs] class MoneyProperty: """ Property for a Money amount. Will return `Money` objects when the property is being get and accepts `Money` objects on set. Value and currency are read/written from/to other fields. Fields are given as locators, that is a string in dotted format, e.g. locator ``"foo.bar"`` points to ``instance.foo.bar`` where ``instance`` is an instance of the class owning the `MoneyProperty`. Setting value of this property to a `Money` object with different currency that is currently set (in the field pointed by the currency locator), will raise an `UnitMixupError`. """ value_class = Money
[docs] def __init__(self, value, currency): """ Initialize MoneyProperty with given field locators. :param value: Locator for value of the Money. :type value: str :param currency: Locator for currency of the Money. :type currency: str """ self._fields = {"value": value, "currency": currency}
def __repr__(self): argstr = ", ".join("{}={!r}".format(*x) for x in self._fields.items()) return f"{type(self).__name__}({argstr})" def __get__(self, instance, type=None): if instance is None: return self return self._get_value_from(instance) def _get_value_from(self, instance, overrides=None): if overrides is None: overrides = {} data = {field: resolve(instance, path) for (field, path) in self._fields.items()} data.update(overrides) if data["value"] is None: return None return self.value_class.from_data(**data) def __set__(self, instance, value): if value is not None: self._check_unit(instance, value) self._set_part(instance, "value", value) def _check_unit(self, instance, value): value_template = self._get_value_from(instance, overrides={"value": 0}) if not value_template.unit_matches_with(value): msg = f"Error! Can't set `{type(self).__name__}` to value with non-matching unit." raise UnitMixupError(value_template, value, msg) assert isinstance(value, self.value_class) def _set_part(self, instance, part_name, value): value_full_path = self._fields[part_name] if "." in value_full_path: (obj_path, attr_to_set) = value_full_path.rsplit(".", 1) obj = resolve(instance, obj_path) else: attr_to_set = value_full_path obj = instance if value is not None: setattr(obj, attr_to_set, getattr(value, part_name)) else: setattr(obj, attr_to_set, None)
[docs] class PriceProperty(MoneyProperty): """ Property for Price object. Similar to `MoneyProperty`, but also has ``includes_tax`` field. Operaters with `TaxfulPrice` and `TaxlessPrice` objects. """ value_class = Price
[docs] def __init__(self, value, currency, includes_tax, **kwargs): """ Initialize PriceProperty with given field locators. :param value: Locator for value of the Price. :type value: str :param currency: Locator for currency of the Price. :type currency: str :param includes_tax: Locator for includes_tax of the Price. :type includes_tax: str """ super().__init__(value, currency, **kwargs) self._fields["includes_tax"] = includes_tax
[docs] class TaxfulPriceProperty(MoneyProperty): value_class = TaxfulPrice
[docs] class TaxlessPriceProperty(MoneyProperty): value_class = TaxlessPrice
[docs] class MoneyPropped: """ Mixin for transforming MoneyProperty init parameters. Add this mixin as (first) base for the class that has `MoneyProperty` properties and this will make its `__init__` transform passed kwargs to the fields specified in the `MoneyProperty`. """
[docs] def __init__(self, *args, **kwargs): transformed = _transform_init_kwargs(type(self), kwargs) super().__init__(*args, **kwargs) _check_transformed_types(self, transformed)
def _transform_init_kwargs(cls, kwargs): transformed = [] for field in list(kwargs.keys()): prop = getattr(cls, field, None) if isinstance(prop, MoneyProperty): value = kwargs.pop(field) _transform_single_init_kwarg(prop, field, value, kwargs) transformed.append((field, value)) return transformed def _transform_single_init_kwarg(prop, field, value, kwargs): if value is not None and not isinstance(value, prop.value_class): raise TypeError(f"Error! Expecting type `{prop.value_class.__name__}` for field `{field}` (got `{value!r}`).") for attr, path in prop._fields.items(): if "." in path: continue # Only set "local" fields if path in kwargs: f = (field, path) raise TypeError("Error! Fields `{}` and `{}` conflict.".format(*f)) if value is None: kwargs[path] = None else: kwargs[path] = getattr(value, attr) def _check_transformed_types(self, transformed): for field, orig_value in transformed: new_value = getattr(self, field) if new_value != orig_value: msg = "Error! Cannot set `%s` to `%r` (try `%r`)." raise TypeError(msg % (field, orig_value, new_value))
[docs] def resolve(obj, path): """ Resolve a locator `path` starting from object `obj`. """ if path: for name in path.split("."): obj = getattr(obj, name, None) return obj