promised

A flexible cached property with get/set/del/init/dependant/cached-mapping capabilities.


License
Apache-2.0
Install
pip install promised==1.2.1

Documentation

Promised

promise

A flexible delayed-evaluation cached property with get/set/del/init capabilities for inter-property relationships.

linked

A dependency-managing promise which will refresh dependent properties when any of its linker methods are called. (typically, deleter and setter)

Member

A cached-mapping extension class designed for the @promise decorator, similar to explicitly (im-)mutable memoization.

Get

Type this into terminal / command-line:

pip install promised

And this into Python:

from promised import promise, linked, Member  # linked are dependent promises, Member is for cached-mapping extension.

Purpose

This project currently functions as an easy method for managing property dependencies, for example:

class _TestLine(object):
    @linked
    def length(self):
        self._length = 2.0
class _TestSquare(object):
    @linked(chain=True)
    def side(self):
        self._side = _TestLine()

    @side.chain("length")
    def width(self):
        self._width = self.side.length

    @side.chain("length")
    def height(self):
        self._height = self.side.length

    @width.linked
    @height.linked
    def area(self):
        self._area = self.width * self.height
class _TestBox(object):
    """This is a test class for linked promises. I don't know what more you're expecting."""
    @linked(chain=True)
    def side(self):
        self._side = _TestLine()

    @linked(chain=True)
    def base(self):
        self._base = _TestSquare()

    @side.chain("length")
    @base.chain("area")
    def volume(self):
        self._volume = self.base.area * self.side.length
def _test_area():
    box = _TestBox()
    assert box.volume == 8.0, "Box volume is 2.0 * 2.0 * 2.0 as Line's default length is 2.0"
    box.side.length = 4
    assert box.volume == 16.0, "Box volume has updated due to change in side's length."
    box.base.side.length = 10
    assert box.volume == 400.0, "Box volume has update due to change in base's side length."
    line = _TestLine()
    line.length = 0.5
    box.side = line
    assert box.volume == 50.0, "Box volume has updated due to changed side."

This started because I found myself doing this too often:

@property
def property_public_name(self):
    '''Why am I typing the same lines with tiny changes in every project all the time?'''
    try:
        return self._property_public_name_with_leading_underscore
    except AttributeError:
        self._property_public_name_with_leading_underscore = self._method_to_calculate_property()
    return self._property_public_name_with_leading_underscore

Usage

Now, it looks like this:

@promise
def property_public_name(self):
    '''Now this is promising!'''
    self._property_public_name_with_leading_underscore = self._method_to_calculate_property()

It's still accessed like this:

property_value = self.property_public_name

And you can still do this:

@property_public_name.setter
@property_public_name.deleter
@property_public_name.getter

You can group a bunch of promises up with the same keeper by passing in the name of the private variable (the variable initially set in the promise's keeper) to the promise's __init__:

def _set_associated_properties(self):
    associated_map_one = {}
    associated_map_two = {}
    for thing in self.iterable:
        associated_map_one = thing.map_one(associated_map_one)
        associated_map_two = thing.map_two(associated_map_two)
    self._property_one_public_name = associated_map_one
    self._property_two_public_name = associated_map_two

property_one_public_name = promised(_set_associated_properties, name="_property_one_public_name")
property_two_public_name = promised(_set_associated_properties, name="_property_two_public_name")

You can link dependent attributes together using an @linked property (which functions similarly to a promised property) and decorating any of the dependent properties' getter / setter / deleter / keeper methods with the @linked_property_name.linked decorator a single time per dependent property:

@linked
def heroes(self):
    self._heroes = None

@heroes.linked
@promise
def future_of_townsville(self):
    self._future_of_townsville = "Bleak" if not self.heroes else "FAN-tastic!"

@future_of_townsville.deleter
def future_of_townsville(self):
    del self._future_of_townsville

@heroes.linker
@heroes.setter
def heroes(self, value):
    self._heroes = value

def test_town_turnaround(self):
    ""Setting self.heroes to a different value should reset its dependent properties."""
    assert not hasattr(self, "_heroes"), "promise should not have already been kept!"
    assert not hasattr(self, "_future_of_townsville"), "promise should not have already been kept!"
    assert self.future_of_townsville == "Bleak", "There should be no heroes - yet!"
    assert self.heroes is None, "There should be no heroes - yet!"
    self.heroes = "POWER-PUFF GIRLS"
    assert not hasattr(self, "_future_of_townsville"), "The future of townsville is dependent on heroes, so it should be deleted once changed!"
    assert self.future_of_townsville == "FAN-tastic!", "The future of townsville should be looking up!"

@linked properties will automatically refresh dependent properties when a @linker method of theirs is called. For ease of use, as this will require at least a deletion method in dependent properties, @linked properties are @promise properties with default deleters and setters which are also default linkers. Using defaults on linked properties, the previous example becomes:

@linked
def heroes(self):
    self._heroes = None

@heroes.linked
def future_of_townsville(self):
    self._future_of_townsville = "Bleak" if not self.heroes else "FAN-tastic!"

def test_town_turnaround(self):
    ""Setting self.heroes to a different value should reset its dependent properties."""
    ...

See documentation in boiler_property.py for further details on removing default deleters / setters / linkers:

@linked(linkers=("keeper",)
def property_which_refreshes_dependent_properties_when_keeper_method_used(self):
    """This would typically reset all dependent properties after this property is accessed for the first time and first access post-refresh/deletion."""
    self._property_which_refreshes_dependent_properties_when_keeper_method_used = "RESET"

@linked(deleter=False, setter=False, linkers=("getter",)
def read_only_property_which_refreshes_dependent_properties_on_every_access(self):
    """Not advised for properties which access this property once reset (as the typical dependent property would.)"""
    self._read_only_property_which_refreshes_dependent_properties_on_every_access = None

You can use the chain=True init argument of @linked properties to designate an inter-class dependency source.

@linked(chain=True)
def side(self):
    self._side = _TestLine()

@linked(chain=True)
def base(self):
    self._base = _TestSquare()

And use @dependency_source.chain("dependent_property_name") to mimic the intra-class behavior of @property_name.linked.

@side.chain("length")
@base.chain("area")
def volume(self):
    self._volume = self.base.area * self.side.length

You can use the Member class to create a cached promised property which varies on input (like memoization, but explicitly mutable / not-mutable):

def _children_of_parent_with_attribute_value(self, parent, child_attribute_value):
    return self.parent_children_map[parent] & self.attribute_value_to_set_of_objects_map[child_attribute_value]

@promise
def adult_children(self):
    self._adult_children = Member(self._children_of_parent_with_attribute_value, "The White House")

Which is then accessed like this:

donnie = countries.adult_children["America"]

Future

These are just the first steps in patterns I've recognized as useful for explicit cached properties, and I'm very interested in building in more automated support for associated & dependent properties - please feel free to share any suggestions.

Copyright

promised module by Andrew M. Hogan. (promised © 2019 Hogan Consulting Group)

License

Licensed under the Apache License.