Source code for shoop.utils.properties

# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.

from shoop.core.pricing import Price, TaxfulPrice, TaxlessPrice
from shoop.utils.money import Money
from shoop.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 = 'Cannot 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('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('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 = '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