Home > other >  How to override module name (__name__)?
How to override module name (__name__)?

Time:09-17

I have two classes in separate files, a.py and b.py.

# a.py
import logging

LOG = logging.getLogger(__name__)

class A:
    def name(self):
        # Do something
        LOG.debug("Did something") # LOG reads __name__ and print it.

I can not modify a.py in any way.

# b.py
import a

class B(A):
    pass

The log output is:

DEBUG:<TIME> called in a, Did something

I want to change __name__ so that a child class's call should log called in b.

My project logs its module name as part of the output. However, when I inherit a class and call the parent class method, I cannot know it is called from A or B because it only shows the parent's module name. How can I change it? Or, some other way to avoid this?

CodePudding user response:

There is a way to do it, but it's more involved than you might think.

Where does the name __name__ come from? The answer is that it's a global variable. This is to be expected, since the global namespace is the module namespace. For example:

>>> globals()['__name__']
'__main__'

If you look up callables in the Standard Type Hierarchy of python's Data Model documentation, you will find that a function's __globals__ attribute is read-only. That means that the function A.name will always look up its global attributes in the namespace of module a (not that the module namespace itself is read-only).

The only way to alter globals for a function is to copy the function object. This has been hashed out on Stack Overflow at least a couple of times:

Copying classes has come up as well:

Solution 1

I've gone ahead and implemented both techniques in a utility library I made called haggis. Specifically, haggis.objects.copy_class will do what you want:

from haggis.objects import copy_class
import a

class B(copy_class(A, globals(), __name__)):
    pass

This will copy A. All function code and non-function attributes will be referenced directly. However, function objects will have the updated __globals__ and __module__ attributes applied.

The major problem with this method is that it breaks your intended inheritance hierarchy slightly. Functionally, you won't see a difference, but issubclass(b.B, a.A) will no longer be true.

Solution 2 (probably the best one)

I can see how it would be a bit cumbersome to copy all the methods of A and break the inheritance hierarchy by doing this. Luckily, there is another method, haggis.objects.copy_func, which can help you copy just the name method out of A:

from haggis.objects import copy_func
import a

class B(A):
    name = copy_func(A.name, globals(), __name__)

This solution is a bit more robust, because copying classes is much more finicky than copying functions in python. It's fairly efficient because both versions of name will actually share the same code object: only the metadata, including __globals__, will be different.

Solution 3

You have a much simpler solution available if you can alter A ever so slightly. Every (normal) class in python has a __module__ attribute that is the __name__ attribute of the module where it was defined.

You can just rewrite A as

class A:
    def name(self):
        # Do something
        print("Did in %s" % self.__module__)

Using self.__module__ instead of __name__ is roughly analogous to using type(self) instead of __class__.

Solution 4

Another simple idea is to properly override A.name:

class B(A):
    def name(self):
        # Do something
        LOG.debug("Did something")

Of course I can see how this would not work well if # Do something is a complex task that is not implemented elsewhere. It's probably much more efficient to just copy the function object directly out of A.

CodePudding user response:

Solution:

Try using inspect.stack:

import inspect
class A:
    def __init__(self):
        pass
    def name(self):
        # Do something
        print("Did in %s" % inspect.stack()[1].filename.rpartition('/')[-1][:-3])

And now running b.py would output:

Did in b

Explanation:

inspect can inspect live objects in python scripts, as metioned in the docs:

Return a list of frame records for the caller’s stack. The first entry in the returned list represents the caller; the last entry represents the outermost call on the stack.

Also it mentioned that it outputs a NamedTuple like:

FrameInfo(frame, filename, lineno, function, code_context, index)

So we could just get the live filename with indexing the second element of this tuple, that would give the full path, so we would have to split it up the path and extract the filename from the full path using str.rpartition.

Edit:

For not editing a.py, you could only try:

b.py:

import inspect
exec(inspect.getsource(__import__('a')))
class B(A):
    pass

And it would give:

Did in b
  • Related