Skip to main content

The agnostic model/view toolshed

Project description

glue.py is a single lightweight utility module expanding Python with new
powerful idioms for accessors, callsets, weak methods, enums, singletons and
observable primitives that can easily be integrated with existing projects. It
is particularly useful for writing model/view oriented applications.

Installation
============

You'll require Python 2.7, Python 3+ or PyPy 1.9 to use glue.py. Earlier
versions are not supported.

glue.py can easily be installed from the Python package repository using
distutils or pip:

pip install glue.py

If you plan to make amendments to the codebase, or you're interested in the
latest development release, it is best to check out the source tree and
install glue.py in development mode:

python setup.py develop

Usage
=====

glue.py provides a larger set of interoperating utility classes and functions,
of which only the most important ones will be described here in detail. It's
not a bad idea to just browse through the source and look what else is there
besides what's covered in this introduction.

Generated Accessors
-------------------

While Python supports accessors through properties, providing regulated access
to class variables has been traditionally cumbersome and repetitive, even though
the syntax has somewhat improved with recent versions. Consider this classical
approach to property access:

from types import NoneType

class Sponge(object):
pass

class Pineapple(object):
on_owner_changed = None

def __init__(self):
self.__owner = None

def get_owner(self):
return self.__owner
def set_owner(self, value):
if value == self.__owner:
return
assert isinstance(value, (Sponge, NoneType))
self.__owner = value
if self.on_owner_changed:
self.on_owner_changed(self)
owner = property(get_owner, set_owner,
doc = "the owner of this Pineapple")

apart from preventing a rewrite, the setter also provides an optional hook that
can be triggered when the value changes. Notice how many times the word "owner"
has to be repeated in one form or the other in order to expose this relatively
simple attribute. Until Python 2.7, most people adapted this handy recipe to
cut down on repetition:

def owner():
doc = """the owner of this pineapple"""
def fget(self):
return self.__owner
def fset(self, value):
if value == self.__owner:
return
assert isinstance(value, (Sponge, NoneType))
self.__owner = value
if self.on_owner_changed:
self.on_owner_changed(self)
return property(**locals())
owner = owner()

That's slightly better, but not perfect, and also a little convoluted. These
days, this approach is the status quo:

@property
def owner(self):
"""The owner of this pineapple"""
return self.__owner
@owner.setter
def owner(self, value):
if value == self.__owner:
return
assert isinstance(value, (Sponge, NoneType))
self.__owner = value
if self.on_owner_changed:
self.on_owner_changed(self)

it seems like we're mostly shuffling details around... all three solutions
vary around 12 lines, and with a bunch of these, classes may seem a lot larger
than they are. This is how glue.py does it:

from glue import (lazydecls, defproperty, autoinit, callset)
from types import NoneType

class Sponge(object):
pass

@lazydecls
class Pineapple(object):
on_owner_changed = callset()

owner = defproperty(
default = None, types=(Sponge, NoneType), hook = on_owner_changed,
doc = "The owner of this pineapple")

def __init__(self):
autoinit()

...case closed. For more information, try `help(glue.defproperty)`.

Callable Sets
-------------

You may have noticed that the `Pineapple` class exposes the `on_owner_changed`
callback as a `callset`, which is just a short way of instantiating a
`CallableSet`. Callable sets are a fast way to create observables:

from glue import callset

def func1(value):
print("func1 called with value",value)
return value+10

def func2(value):
print("func2 called with value",value)
return value+20

def func3(value):
print("func3 called with value",value)
return value+30

Trying this on the command line:

>>> funcs = callset([func1,func2,func3])
>>> print(max(funcs(10)))
func1 called with value 10
func2 called with value 10
func3 called with value 10
40

In the pineapple example, this is how a view would typically subscribe to
events sent by the `Pineapple` class:

@lazydecls
class Squid(object):
neighbor_house = defproperty()
annoying_coworkers = defproperty(default = set)

def __init__(self, **kwargs):
# auto-assign matching keyword args
autoinit(**kwargs)
# track if the owner of the neighbor house changes by registering
# Squid's callback. The callback will be weakly proxied, and
# automatically unregisters when this Squid instance is no longer
# referenced.
self.neighbor_house.on_owner_changed.addweak(self.someone_moved_in)

def someone_moved_in(self, event):
# since the on_owner_changed callset is global to Pineapple, Squid
# would be notified of any owner changes in Pineapples, so filter
# for the right one.
if not (event.instance is self.neighbor_house):
return
# check if the new tenant is one of our annoying coworkers
if event.value in self.annoying_coworkers:
# state opinion on the situation
print("Meh.")


Trying this on the command line:

>>> house = Pineapple()
>>> bob = Sponge()
>>> squidward = Squid(neighbor_house = house)
>>> squidward.annoying_coworkers.add(bob)
>>> house.owner = bob
Meh.

And that's the gist of it.

Weak Callable Proxies
---------------------

TODO

Observable Dicts, Lists and Sets
--------------------------------

TODO

Enums
-----

TODO

Singletons
----------

TODO

Project details


Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page