Skip to main content

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.

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

License

Licensed under the Apache License.

Metadata

Release files for promised 1.2.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for promised 1.2.2
File Size Uploaded Provenance Actions
promised-1.2.2.tar.gz 18.4 kB Without provenance Details for promised-1.2.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for promised 1.2.2
File Interpreter ABI Platform Actions
promised-1.2.2-py3-none-any.whl Python 3 none any Details for promised-1.2.2-py3-none-any.whl

Total release size: 36.4 kB

Release files / promised-1.2.2.tar.gz

Download URL promised-1.2.2.tar.gz
Size 18.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1b3b379d939944b42855e258f8b34ec59895c5478f93beb1411a91dd6cedcf85
BLAKE2b-256 checksum
How to use checksums
1873e5fecffc11ca9cbf14f528a53ed7187e7cedba603b6842398fe0264913b4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/1.13.0 pkginfo/1.4.2 requests/2.21.0 setuptools/40.0.0 requests-toolbelt/0.8.0 tqdm/4.23.3 CPython/3.6.6

Release files / promised-1.2.2-py3-none-any.whl

Download URL promised-1.2.2-py3-none-any.whl
Size 17.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ba168973f022b4316ba243ffac55cc184d26bda63968a2c91b09c176c602066b
BLAKE2b-256 checksum
How to use checksums
75d9b361361dfcc56697fce0d159caa805f270af75b2d11be28c75f4a2a6cb0a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/1.13.0 pkginfo/1.4.2 requests/2.21.0 setuptools/40.0.0 requests-toolbelt/0.8.0 tqdm/4.23.3 CPython/3.6.6

Security for promised 1.2.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page