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.
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
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.
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
You may have noticed that the
Pineapple class exposes the
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
@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
Observable Dicts, Lists and Sets