Verifying Access to Private Methods
This is where I make a big messy knot of all the stuff discussed earlier and spit some code, which I hope will be useful. Here is the plan:
- Write access verification code based on call stack frame inspection
- Define a meta-class that inject access verification code to private methods
- Define a base class that has the aforementioned meta-class
- All classes that should be checked will inherit from the base class
contains the CodeAccessVerifier module. The module consists of the CodeAccessViolation exception, the Metaclass [meta]class, the AccessChecker class, and the _decorate
The CodeAccessError exception is raised when an out-of-class code tries to call a private method. It contains the frame info (method, filename, class) of the called private method and the caller.
The __init__() method of the Metaclass kicks into action when any application class that inherits from the CodeChecker class is created. It iterates over all the methods of the application class and replaces all the private ones with the decorated function returned from _decorate(). Note the commented lines that check if a certain environment variable exists and return immediately if not. This is a quick and dirty way to enable/disable the meta-functionality by nipping it in the bud.
The _decorate() function gets a naked private method and returns a nested function called decorated(), which calls _verify and then the original private method (called func in this context). Note that func can have any signature since decorated() utilizes the *args, **kw arguments that allow an arbitrary number of arguments to be passed. _decorate() pulls a nifty little trick out of its sleeve: It adds an attribute called func_class that contains the class object to the original function object (func). The reason will be clear soon.
The _verify() function is called now whenever a decorated function (originally a private method) is called, just before the original method. _verify() calls _getFrameInfo() with depth 1 and 2 to get the frame information for the decorated private method and its caller. If the caller is a method of the same class (from the same file) all is well, otherwise it raises the dreaded CodeAccessError.
The _getFrameInfo() function is pretty similar to getCallerInfo() from Listing 2. The difference is that it expects a depth argument (getCallerInfo() always extracted from depth 2) and that it is aware of the custom 'func_class' attribute that _decorate() injected to the function object. It tries to retrieve the func_class attribute to get the class that corresponds to frame's method (if it is indeed a method). The original function 'func' is available in the locals() dictionary (args). If there is no 'func' or no 'func.func_class' it tries to guess the class by assuming the first argument is the 'self' argument and its type is the class.
Note the subtle navigation to various objects starting with the frame object. If the 'func' attribute exists and it has the custom 'func_class' attribute then you are dealing with the wrapped function and you get all information from func.func_code (the code object associated with the wrapped function). Otherwise, it's a caller and the information is retrieved directly from f.f_code (the code object associated with caller function).
Will you take my word that it works? Yeah, I didn't think so. I guess I'll have to show you. Listing 4 runs the CodeAccessVerfier through its paces. There is a class A that inherits from CodeAccessVerifier.AccessChecker. Class A has two methods: a public method add() and a private method __internalAdd(). The add() method simply calls __internalAdd() that returns the sum x+y (it's allowed because they both belong to the same class). The main() function instantiates A and calls both methods. As you can see the call to a.add() returns properly where the call to a._A__internalAdd() raises the CodeAccessError exception.
Now do you believe me? You shouldn't. I withheld one detail. You can cheat the code access check by writing a function in the same file as the class that expects an instance of the target class as its first argument. Such a function will be considered by CodeAccessVerifier a valid caller.
Final Pearls of Wisdom
Python has an interesting type system and execution model. It gives you a lot of leeway to push the envelope and change just about anything at runtime. You should not abuse this power. Follow the principle of least surprise. Write readable code and try to avoid invisible side effects.
It is OK for frameworks and infrastructure code to transform and inject stuff quietly under the covers, but it is generally a big no-no for application code. There is no justification to implement your business logic by dynamically contacting the Budapest office, compiling on the fly their latest base class, and deriving your class and adding a couple of new methods for good measure. You will never know what hit you when the first bug shows up.