Chaining monkey patches in Python: partial legitimization of a venial sin

Monkey patching is a spectacular way to modify third-party code, but a terrible way to make friends (I here of course discount the pervasive problems coders have with socialization in general). Not only does it make issues notoriously difficult to track down, dirtying otherwise clean code with obscurity, but it's arrogant to boot. You've overridden, in perpetuity, any changes made to that third-party code that tread on the territory your monkey patch has conquered. More likely than not, you've rendered upgrades of the patchee untenable without breaking your code or their code or both.

Nonetheless, it's bloody useful, when one exerts a certain degree of control over the process. We use the technique in Zenoss quite a lot, in our ZenPack plugins, which often need to extend the functionality of the main product; since ZenPacks can be created by anyone with Zenoss and a dream, adding hooks into Core manually isn't an option, so ZenPacks have to monkey-patch in their features. It's fine, really. We only feel a little dirty. Less dirty since we started using a decorator!

def monkeypatch(target):
def patcher(func):
setattr(target, func.__name__, func)
return func
return patcher

import MyClass

@monkeypatch(MyClass)
def myfunc():
...

It's pretty trivial, and I'm sure there are myriad identical decorators out there; I think Guido even wrote one at some point. Nonetheless, it makes the process look much less ugly (which is, interestingly, one of the reasons anti-monkey-patching zealots reject such a decorator. They believe that such an ugly practice should look ugly, to deter usage. As if).

Today I was presented with a case where monkey-patching could actually be considered beneficial. I needed to validate manual addition of devices to Zenoss, to make sure they weren't put into a reserved device class. Whatever, no big deal, except it occurred to me that other ZenPacks were likely to override the function as well, which would knock out my validation (since monkey-patching involves replacing existing methods wholesale, he who monkey-patches last wins, and all other patches are discarded). Thus I needed some way to chain monkey patches.

This could, in fact, be considered something of a feature. If a few standard hooks were set up, meant to be monkey-patched an arbitrary number of times, aggregating results, ZenPacks could play nicely with each other—in this case, monkey patching is really the best way to do it.

It only took a few minutes to come up with the decorator:

def monkeypatch_reduce(target, reducefunc):
"""
A decorator factory that chains monkey patches, in the
case where separate patches may modify the same class.
"""
def patcher(func):
try:
_oldfunc = getattr(target, func.__name__)
except AttributeError:
setattr(target, func.__name__, func)
return func
def inner(*args, **kwargs):
return reducefunc(
_oldfunc(*args, **kwargs),
func(*args, **kwargs))
setattr(target, func.__name__, inner)
return inner
return patcher

With this decorator, you can monkey patch a class over and over, using any reduction function you want, and get the reduced result. For example, my validation function would use a reduction function that returns True if all criteria validate, and False otherwise:

from Products.ZenModel import Device

@monkeypatch_reduce(Device, reducefunc=lambda x,y:x and y)
def validate_device(dev):
return dev.fulfills_some_criteria()

@monkeypatch_reduce(Device, reducefunc=lambda x,y:x and y)
def validate_device(dev):
return dev.fulfills_other_criteria()

Any ZenPack can add their criteria to the validation process by monkey patching with this decorator, without worrying at all about anybody else's monkey patches.

One could also create a hook that would be monkey-patched to provide a kind of registry:

ZenPack.py:
-----------
def zenpack_list(self):
return []

A ZenPack:
----------
from Products.ZenModel import ZenPack
@monkeypatch_reduce(ZenPack, operator.add)
def zenpack_list(self):
return ["A Pack"]

A different ZenPack:
--------------------
from Products.ZenModel import ZenPack
@monkeypatch_reduce(ZenPack, operator.add)
def zenpack_list(self):
return ["A Different Pack"]


See? Each acts as if it's providing the only result, and the patch chain reduces them into a list:

>>> ZenPack.zenpack_list()
["A Pack", "A Different Pack"]

I dunno, maybe it's crazy. But I think it's got legs. I'm going to play around with it a bit more.

Read More...

Trivial but useful: One-line ssh key setup

For some reason, a lot of people have issues setting up ssh key authentication on remote boxes; sometimes it's cut-and-paste problems, sometimes mistakes editing the file manually. Also some aren't aware that you don't need to push your pubkey file up to the box, then ssh in and copy/paste the contents into authorized_keys. You can do it all in one go:

$ cat ~/.ssh/id_dsa.pub | ssh user@remotebox \
"cat - >> ~/.ssh/authorized_keys"

A minor improvement to a small problem, perhaps, but it removes a bunch of failure-prone steps.

Update: I wrote a function that pushes your key out to the remote box, then modifies your ~/.ssh/config to use the specified username:

setupssh () {
USER=${1%@*}
BOX=${1#*@}
if [ "$USER" = "$1" ]; then
USER=`whoami`
else
# Set up user
echo "
Host $BOX
User $USER" >> $HOME/.ssh/config
fi
# Install the key
cat ~/.ssh/id_dsa.pub | ssh $USER@$BOX "cat - >> ~/.ssh/authorized_keys"
}

Shove that in your ~/.bashrc, source it, and type:

$ setupssh remoteuser@my.remotebox.com

You'll have to enter remoteuser's password once when it pushes out your pubkey. From then on, you can just ssh my.remotebox.com, no passwords.

Read More...

cliutils 0.1.3 released: Persistence, home directories

This release of cliutils adds a few handy items. The new persistence module provides a function (storage_dir) that will, on Windows or *nix, find a directory where settings files should be stored (namely, the roaming Application Data directory on Windows, or the current user's home directory on *nix). Not too complex, but it removes a bit of overhead in locating the proper storage path. Also handy for creating directories; if the result doesn't exist, it'll be created.

The persistence module also provides a function that uses the inimitable shelve module to create a persistent dictionary, getting its path from storage_dir. This lets one create very simple databases on the fly for storing settings and whatnot, like:

>>> db('.myscript')
{}


Since the dictionary is saved whenever changed, it can be used just like a regular dictionary, only persistent. I've found this very useful.

Finally, the persistence module provides a simple dictionary-like interface to ConfigParser instances, again getting a path through storage_dir:

>>> import tempfile; filename = tempfile.mkstemp()[1]
>>> cfg = config(filename)
>>> cfg['sec1']['option2'] = 75
>>> cfg['sec2']['option1'] = "Some String"
>>> cfg['sec2']['option2'] = "Another value"
>>> f = file(filename)
>>> print f.read()
[sec1]
option2 = 75

[sec2]
option2 = Another value
option1 = Some String


Lastly, at the suggestion of a commenter, the logged decorator has been renamed to the more accurate redirect. Also, log_decorator has been deprecated, since it was really just a synonym of logged.

Download from Google Code or pypi. Upgrade or install anew with:

easy_install -U cliutils


API docs are here.

Read More...