Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

From python, can we track module-level assignments before (other) user code executes?

I'm working on a tool that would benefit from the ability to track all references to a given object from within python.

Specifically, I'd like to make a test doubles system that could replace all module-level attributes of a given type. For example, assume the following code is in module c:

from a import b

If a is a module, b is a reference to the object named a.b, but it is a separate reference. If my test double system later replaces a.b, c.b will still refer to the original object.

I would like to have my tool track all assignments of a.b to aliases, but module-level aliasing would go a long way toward my goal.

Metaphorically, what I'd like is to override Module.__setattribute__:

def __setattribute__(self, name, value):
    if isinstance(value, interesting_types):
        # remember this use of the interesting object and call super for normal processing.

Assume that I can get my code loaded before modules which might be tracked can be loaded.

like image 919
Jeremy Dunck Avatar asked Apr 06 '14 16:04

Jeremy Dunck


1 Answers

This sort of thing may work for you. First, some code:

a.py

b = 42
# other module definitions

fakery.py

class Fakery(object):
    def __init__(self, mod):
        self.module = __import__(mod)
        import sys
        sys.modules[self.module.__name__] = self

    def __getattr__(self, name):
        print "__getattr__ called with '%s'" % name
        result = getattr(self.module, name)
        if name == 'b':
            result += 1
        return result

Example

>>> import fakery
>>> fakery.Fakery('a')
<fakery.Fakery object at 0x109007110>
>>> from a import b
__getattr__ called with '__path__'
__getattr__ called with 'b'
>>> print b
43
>>> import a
>>> print a
<fakery.Fakery object at 0x109007110>

All you would have to do is modify the Fakery class to do whatever it is that you want to do. In this case, I'm just adding 1 to a's b.

I hope it's clear how this works, but here's a quick explanation.

When a module is imported, a record of it gets stuffed into sys.modules. When you instantiate a Fakery object, what it does is import a module by name (using __import__), and then replaces that module's entry in sys.modules with itself.

Python only imports modules once, storing the imported module in sys.modules. Every import after the first returns the entry in sys.modules. The Fakery object inserts itself into sys.modules['a'], taking the place of the real module. All subsequent import a or from a import <whatever> statements are now directed to an instance of Fakery. Since it's just a class, you can do all kinds of crazy stuff using magic methods or metaprogramming.

__getattr__ is convenient because it gets called when a requested attribute doesn't exist.

like image 54
Seth Avatar answered Sep 27 '22 19:09

Seth