Source code for shuup.utils.properties

# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.

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


[docs]class MoneyProperty(object): """ 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 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("%s=%r" % x for x in self._fields.items()) return "%s(%s)" % (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={}): 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 = "Error! Can't set `%s` to value with non-matching unit." % (type(self).__name__,) 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 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(PriceProperty, self).__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(object): """ 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`. """ def __init__(self, *args, **kwargs): transformed = _transform_init_kwargs(type(self), kwargs) super(MoneyPropped, self).__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( "Error! Expecting type `%s` for field `%s` (got `%r`)." % (prop.value_class.__name__, field, value) ) 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 `%s` and `%s` conflict." % 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