Dec 15, 2015
2 minutes read

This post is about pyintercept. A tool that I created to intercept function calls in Python scripts without modifying its source code. It basically patches the bytecode before executing.

A little history

When I started to develop Codenizer, the first challenging feature I faced was the dependency tracking. The tracking part is almost trivial, but extracting dependencies without actually running anything, like python setup.py install is hard.

I could install it in an isolated environment like Docker, and then check the installed libraries by doing something like pip freeze but I wanted to avoid the hassle of compiling heavy-weight stuff just to know its dependencies.

Installation

pip install pyintercept

Usage

This is an example of how to use pyintercept to get the Wagtail dependencies.

$ git clone https://github.com/torchbox/wagtail.git # Wagtail 1.2
$ cd wagtail
$ python -m pyintercept setup.py setuptools.setup --args=install --handler=pyintercept.pdb

Where:

  • setuptools.setup is the dotted path to the function we want to intercept
  • --args=install is used to pass arguments to the script (setup.py)
  • --handler=pyintercept.pdb is used to set the handler you want. There are some predefined handlers: json, pdb, pickle and print.

It will drop you in a pdb console:

(Pdb) l
1  	def pdb(origfn, *args, **kwargs):
2  	    import pdb; pdb.set_trace()
3  ->       return origfn(*args, **kwargs)
(Pdb) p origfn
<function setup at 0x1020ed9b0>
(Pdb) from pprint import pprint
(Pdb) pprint(kwargs['install_requires'])
['Django>=1.7.1,<1.10',
'django-compressor>=1.4',
'django-modelcluster>=1.0',
'django-taggit>=0.17.5',
'django-treebeard==3.0',
'djangorestframework>=3.1.3',
'Pillow>=2.6.1',
'beautifulsoup4>=4.3.2',
'html5lib>=0.999,<1',
'Unidecode>=0.04.14',
'Willow>=0.2.2,<0.3']

Okay, so we just intercepted the call to setuptools.setup without touching any code. The origfn argument contains the original function. The *args and **kwargs contains the arguments that would be passed to the original function.

Advanced usage

Defining a custom handler

You can also, of course, write your own handlers. Let’s write a custom handler that prints out to stdout all requirements in JSON format.

Let’s call it amazing.py:

def handler(*args, **kwargs):
    import json
    print(json.dumps(kwargs.get('install_requires')))
python -m pyintercept setup.py setuptools.setup --args=install --handler=amazing.handler
["Django>=1.7.1,<1.10", "django-compressor>=1.4", "django-modelcluster>=1.0", "django-taggit>=0.17.5", "django-treebeard==3.0", "djangorestframework>=3.1.3", "Pillow>=2.6.1", "beautifulsoup4>=4.3.2", "html5lib>=0.999,<1", "Unidecode>=0.04.14", "Willow>=0.2.2,<0.3"]

It’s important to notice the import statement inside the handler function. You’ll have to ensure that all the required stuff to make your handler work is defined within it, otherwise it will not be injected and will cause errors.


Back to posts


comments powered by Disqus