hear no evil, see no evil, patch no evil: or, how to monkey-patch safely

50
Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely. Graham Dumpleton @GrahamDumpleton PyCon Australia - August 2016

Upload: graham-dumpleton

Post on 13-Apr-2017

187 views

Category:

Software


5 download

TRANSCRIPT

Page 1: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Hear no evil, see no evil, patch no evil: Or, how to

monkey-patch safely.

Graham Dumpleton @GrahamDumpleton

PyCon Australia - August 2016

Page 2: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Decorators are useful!

Page 3: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Decorators are easy to implement?

Page 4: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Are you sure?

Page 5: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Typical decorator.

def function_wrapper(wrapped): def _wrapper(*args, **kwargs): return wrapped(*args, **kwargs) return _wrapper

@function_wrapper def function(): pass

Page 6: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

This breaks introspection.

Page 7: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

__name__ and __doc__ attributes are not

preserved.

Page 8: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Doesn’t @functools.wraps() help?

import functools

def function_wrapper(wrapped): @functools.wraps(wrapped) def _wrapper(*args, **kwargs): return wrapped(*args, **kwargs) return _wrapper

@function_wrapper def function(): pass

Page 9: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

No, it doesn’t solve all problems.

Page 10: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Still issues with: introspection, wrapping decorators

implemented using descriptors, and more.

Page 11: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

http://blog.dscpl.com.auQuick Link: Decorators and monkey patching.

Complicated details removed ….

Get all the details at:

Page 12: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Please try not to implement decorators

yourself.

Page 13: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

What is the solution?

Page 14: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Use ‘wrapt’.

Page 15: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Basic decorator.

import wrapt

@wrapt.decorator def pass_through(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

@pass_through def function(): pass

Page 16: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Universal decorator.import wrapt import inspect

@wrapt.decorator def universal(wrapped, instance, args, kwargs): if instance is None: if inspect.isclass(wrapped): # Decorator was applied to a class. return wrapped(*args, **kwargs) else: # Decorator was applied to a function or staticmethod. return wrapped(*args, **kwargs) else: if inspect.isclass(instance): # Decorator was applied to a classmethod. return wrapped(*args, **kwargs) else: # Decorator was applied to an instancemethod. return wrapped(*args, **kwargs)

Page 17: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Bonus feature of wrapt if using multithreading.

Page 18: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Synchronise function calls.

from wrapt import synchronized

@synchronized # lock bound to function1 def function1(): pass

@synchronized # lock bound to function2 def function2(): pass

Page 19: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Methods of classes as well.from wrapt import synchronized

class Class(object):

@synchronized # lock bound to instance of Class def function_im(self): pass

@synchronized # lock bound to Class @classmethod def function_cm(cls): pass

@synchronized # lock bound to function_sm @staticmethod def function_sm(): pass

Page 20: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Synchronise block of code.from wrapt import synchronized

class Object(object): @synchronized def function_im_1(self): pass

def function_im_2(self): with synchronized(self): pass

def function_im_3(self): with synchronized(Object): pass

Page 21: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Don’t trust me when I say you should use wrapt?

Page 22: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Potential candidate for being included in the Python

standard library.

So it must be awesome.

Page 23: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Primary purpose of the wrapt package wasn’t as way to build decorators.

Page 24: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Primary reason for existence of wrapt was to

help with monkey patching.

Page 25: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Decorators rely on similar principles to monkey

patching.

Page 26: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Before decorators.# python 2.4+

@function_wrapper def function(): pass

# python 2.3

def function(): pass function = function_wrapper(function)

Page 27: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Decorators are applied when code is defined.

Page 28: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Monkey patching is performed after the fact,

… and can’t use the decorator syntax

Page 29: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Why monkey patch?

• Fix bugs in code you can’t modify.

• Replace/mock out code for testing.

• Add instrumentation for monitoring.

Page 30: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Monkey patching with wrapt.# example.py

class Example(object): def name(self): return 'name'

# patches.py

import wrapt

def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

from example import Example

wrapt.wrap_function_wrapper(Example, 'name', wrapper)

Page 31: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Don’t patch it yourself.# patches.py

import wrapt

@wrapt.decorator def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

from example import Example

# DON’T DO THIS.

Example.name = wrapper(Example.name)

Page 32: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Direct patching of methods breaks in

certain corner cases.

Let wrapt apply the wrapper for you.

Page 33: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Avoiding imports.

# patches.py

import wrapt

@wrapt.patch_function_wrapper('example', 'Example.name') def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

Page 34: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

What about testing, where we do not want permanent patches?

Page 35: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Mock alternative.# example.py

class Storage(object): def lookup(self, key): return 'value' def clear(self): pass

# tests.py

import wrapt

@wrapt.transient_function_wrapper('example', 'Storage.lookup') def validate_storage_lookup(wrapped, instance, args, kwargs): assert len(args) == 1 and not kwargs return wrapped(*args, **kwargs)

@validate_storage_lookup def test_method(): storage = Storage() result = storage.lookup('key') storage.clear()

Page 36: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

What if we need to intercept access to single

instance of an object?

Page 37: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Transparent object proxy.

# tests.py

import wrapt

class StorageProxy(wrapt.ObjectProxy): def lookup(self, key): assert isinstance(key, str) return self.__wrapped__.lookup(key)

def test_method(): storage = StorageProxy(Storage()) result = storage.lookup(‘key') storage.clear()

Page 38: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Beware though of ordering problems when

applying monkey patches.

Page 39: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Import from module.# example.py

def function(): pass

# module.py

from example import function

# patches.py

import wrapt

@wrapt.patch_function_wrapper('example', 'function') def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

Page 40: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Plus importing and patching of modules that the

application doesn’t need.

Page 41: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Post import hooks (PEP 369).

# patches.py

import wrapt

def wrapper(wrapped, instance, args, kwargs): return wrapped(*args, **kwargs)

@wrapt.when_imported('example') def apply_patches(module): wrapt.wrap_function_wrapper(module, 'Example.name', wrapper)

Page 42: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Better, but still requires patches module to be

imported before anything else in the main

application script.

Page 43: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Need a way to trigger monkey patches without

modifying application code.

Page 44: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

The autowrapt package.

pip install autowrapt

Page 45: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Bundle patches as module.

from setuptools import setup

PATCHES = [ 'wsgiref.simple_server = wrapt_wsgiref_debugging:apply_patches' ]

setup( name = 'wrapt_wsgiref_debugging', version = '0.1', py_modules = ['wrapt_wsgiref_debugging'], entry_points = {‘wrapt_wsgiref_debugging': PATCHES} )

Page 46: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Patch to time functions.from __future__ import print_function from wrapt import wrap_function_wrapper from timeit import default_timer

def timed_function(wrapped, instance, args, kwargs): start = default_timer() print('start', wrapped.__name__) try: return wrapped(*args, **kwargs) finally: duration = default_timer() - start print('finish %s %.3fms' % ( wrapped.__name__, duration*1000.0))

def apply_patches(module): print('patching', module.__name__)

wrap_function_wrapper(module, 'WSGIRequestHandler.handle', timed_function)

Page 47: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Enabling patches.

$ AUTOWRAPT_BOOTSTRAP=wrapt_wsgiref_debugging $ export AUTOWRAPT_BOOTSTRAP

$ python app.py

patching wsgiref.simple_server start handle 127.0.0.1 - - [14/Jul/2016 10:18:46] "GET / HTTP/1.1" 200 12 finish handle 1.018ms

Page 48: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Packaging of patches means they could technically be

shared via PyPi.

Eg: instrumentation for monitoring.

Page 49: Hear no evil, see no evil, patch no evil: Or, how to monkey-patch safely

Reasons to use wrapt.

• Create better decorators.

• Awesome thread synchronisation decorator.

• Safer mechanisms for monkey patching.