Skip to main content

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

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Files for promised, version 1.2.2
Filename, size File type Python version Upload date Hashes
Filename, size promised-1.2.2-py3-none-any.whl (17.9 kB) File type Wheel Python version py3 Upload date Hashes View
Filename, size promised-1.2.2.tar.gz (18.4 kB) File type Source Python version None Upload date Hashes View

Supported by

Pingdom Pingdom Monitoring Google Google Object Storage and Download Analytics Sentry Sentry Error logging AWS AWS Cloud computing DataDog DataDog Monitoring Fastly Fastly CDN DigiCert DigiCert EV certificate StatusPage StatusPage Status page