""" open/dulcinea/lib/property/property_template.py """ from dulcinea.base import DulcineaPersistent from dulcinea.material import get_material_db from dulcinea.physical_unit import get_standard_unit, PhysicalUnit from dulcinea.physical_value import PhysicalValue from qp.lib.spec import add_getters, string, pattern, both from qp.lib.spec import boolean, instance, anything, spec, specify from qpy import stringify import dulcinea.property.property_type # Default value for 'check_constraint()' method -- can't use None because # it's a legitimate constraint. _no_value = "_no_value" class PropertyTemplate (DulcineaPersistent): """ Property templates are used to guide property input. """ constraint_is = spec( (None, list), "a list of allowed values for the property") description_is = spec( (None, string), "an accurate, concise, technical, readable description of the " "property") def __init__(self): assert self.__class__ is not PropertyTemplate, "abstract class" def get_description(self): return self.description def set_description(self, description): specify(self, description=description) def check_type(self, value): """(value : any) Ensure that 'value' is compatible with the property type. Raise PropertyTypeError if not. """ self.get_type().check_value(value) def is_atomic(self): return self.get_type().is_atomic() def is_list(self): return self.get_type().is_list() def is_table(self): return self.get_type().is_table() def is_aggregate(self): return self.get_type().is_aggregate() def is_input(self): return 0 def is_property(self): return 0 def set_constraint(self, set): """(set : [property_value]) Specify a constraint. All elements of the constraint must be of the same type or a range, which must be the correct type for the property to which this constraint belongs. For a value to meet the constraint, it must be equal to or within the range of one of the elements. """ self.check_constraint(set) specify(self, constraint=set) def get_constraint(self): """Return the constraint or None if there is no constraint. """ raise NotImplementedError def get_constraint_unit(self): constraint = self.get_constraint() if (constraint and isinstance(constraint[0], PhysicalValue)): return constraint[0].get_unit() else: return None def get_constraint_range(self): """() -> PhysicalValue Return the range of valid values described by the constraint. If it's unconstrained, return None otherwise return the span of the set. """ return self.get_type().get_constraint_range(self.get_constraint()) def is_discrete(self): """Return true if this property can only take on discrete values. If the constraint contains range values or there is no constraint then false is returned.""" return self.get_type().is_discrete(self.get_constraint()) def format(self, html=0): """Return a string with a brief summary of the property constraint, suitable to explain the constraint to a user before they supply a value. Return None if constraint is empty. """ return self.get_type().format_constraint(self.get_constraint(), html=html) def explain_constraint(self, html=0): """Return a string with a fairly detailed explanation of the property constraint, suitable for use in error messages after user has violated the constraint. Return None if constraint is empty. """ constraint = self.get_constraint() return self.get_type().explain_constraint(constraint, html=html) def is_allowed_unit(self, unit): """Return true if 'unit' is an allowed unit for properties derived from this property template, false otherwise. (If the 'allowed_units' attribute is not defined, no units are allowed). """ allowed_units = self.get_allowed_units() if allowed_units is None: return unit is None elif unit is None: return 0 else: unit = get_standard_unit(unit) return unit in allowed_units def check_value(self, value): """(value : property_value) Checks if 'value' is of the allowed type and meets the constraint specified earlier with 'set_constraint()'. Returns silently if no constraint was set, or if the value meets the constraint. Raises ConstraintError if a constraint is defined, but 'value' doesn't meet it. Raises PropertyTypeError (indirectly) if the value is of the wrong type. """ if value is None: return # Raises PropertyTypeError if type is wrong. self.check_type(value) constraint = self.get_constraint() if constraint is not None: self.get_type().check_value_in_constraint(value, constraint) def get_default_unit(self): return self.get_type().get_unit() def is_time(self): """ Return true if this property is a PhysicalValue and the units are time units. """ unit = self.get_default_unit() return unit and unit.is_time() def simplify(self): """() -> RangeValue | None Return a simplified version of this template's constraint (a RangeValue containing ints or floats). Return None if there is no constraint or if the constraint cannot be simplified. """ property_type = self.get_type() if property_type.is_int() or property_type.is_physical_value(): constraint_range = self.get_constraint_range() if constraint_range is not None: return property_type.simplify_value(constraint_range) return None def create_value(self, value=None): """Create a new Property based on this template.""" from dulcinea.property.property import Property return Property(self, value) def __str__(self): constraint = self.get_constraint() if constraint: constraint = list(map(stringify, constraint)) if len(constraint) > 5: constraint = (", ".join(constraint[0:3]) + " ... " + constraint[-1]) else: constraint = ", ".join(constraint) return "%s : set %s" % (self.get_name(), constraint) else: return "%s (unconstrained)" % self.get_name() class MasterTemplate (PropertyTemplate): name_is = spec( both(string, pattern('^[a-z][a-z0-9_]*$')), "a software-friendly name for the property.") type_is = spec( instance('PropertyType'), "the type of this property") hidden_is = spec( boolean, "a UI hint to show the property less often") title_is = spec( (None, string), "the property name in a human-friendly form (if None, get_title " "derives the title from the name)") def __init__(self, name, property_type, hidden=0, description=None, title=None, constraint=None): self.set_name(name) if title is None: words = self.name.split("_") words[0] = words[0].capitalize() title = " ".join(words) specify(self, type=property_type, hidden=0, description=description, title=title, constraint=constraint) def get_allowed_units(self): units = self.get_type().get_compatible_units() if units: return [unit for unit in units if not unit.artificial] else: return None def get_master(self): return self def is_hidden(self): return self.hidden def set_name(self, name): specify(self, name=name) def set_hidden(self, hidden): specify(self, hidden=hidden) def set_title(self, title): specify(self, title=title) def get_constraint(self): """Return the constraint or None if there is no constraint. """ if self.constraint is None and self.get_type().is_material(): return list(get_material_db().get_materials()) else: return self.constraint def create_input_template(self, constraint=None, description=None, allowed_units=None, required=0, default_value=None): """Create a new InputTemplate based on this template.""" constraint = constraint or self.constraint return InputTemplate(self, constraint=constraint, description=description, allowed_units=allowed_units, required=required, default_value=default_value) def check_constraint(self, set=_no_value): if set is _no_value: set = self.constraint self.get_type().check_constraint(set) def is_required(self): return 0 add_getters(MasterTemplate) class InputTemplate (PropertyTemplate): """ Input templates are created to guide input. """ template_is = spec( MasterTemplate, "the master template for this template; this must always be set") allowed_units_is = spec( (None, [PhysicalUnit]), "a list of PhysicalUnit instances that controls what unit may be " "used when entering a property.") required_is = spec( boolean, "true if a value is required for this property") default_value_is = spec( anything, "A typical value for this property") def __init__(self, template, constraint=None, description=None, allowed_units=None, required=0, default_value=None): specify(self, template=template, default_value=default_value, description=description, required=required) self.set_allowed_units(allowed_units) self.set_constraint(constraint) def check_constraint(self, set=_no_value): """Verify that set is a valid constraint for this template. """ if set is None: return if set is _no_value: set = self.constraint self.get_type().check_set_in_constraint( set, self.template.get_constraint()) def get_template(self): return self.template def get_master(self): return self.template.get_master() def get_name(self): return self.template.get_name() def set_default_value(self, value): self.check_value(value) self.default_value = value def check_default_value(self): self.check_value(self.default_value) def get_default_value(self): if self.default_value == '': return None return self.default_value def is_hidden(self): return self.get_master().is_hidden() def is_required(self): return self.required def is_input(self): return 1 def get_title(self): return self.template.get_title() def get_description(self): return self.description or self.template.get_description() def get_type(self): return self.template.get_type() def set_allowed_units(self, units): """(units : [PhysicalUnit | string] | None)""" if not units or units == [None]: self.allowed_units = None else: assert self.get_type().is_physical_value(), ( 'allowed_units only apply to physical value properties ' 'not %r' % self.get_type()) self.allowed_units = [get_standard_unit(unit) for unit in units] def get_allowed_units(self): return (self.allowed_units or self.template.get_allowed_units()) def get_constraint(self): """Return the constraint or None if there is no constraint. """ if self.constraint is None: return self.template.get_constraint() else: return self.constraint def set_required(self, required): specify(self, required=required) def create_default_value(self): """Create a new Property based on this template.""" from dulcinea.property.property import Property return Property(self, self.get_default_value())