2

I have a function called get_full_class_name(instance), which returns the full module-qualified class name of instance.

Example my_utils.py:

def get_full_class_name(instance):
    return '.'.join([instance.__class__.__module__,
                     instance.__class__.__name__])

Unfortunately, this function fails when given a class that's defined in a currently running script.

Example my_module.py:

#! /usr/bin/env python

from my_utils import get_full_class_name

class MyClass(object):
    pass

def main():
    print get_full_class_name(MyClass())

if __name__ == '__main__':
    main()

When I run the above script, instead of printing my_module.MyClass, it prints __main__.MyClass:

$ ./my_module.py
__main__.MyClass

I do get the desired behavior if I run the above main() from another script.

Example run_my_module.py:

#! /usr/bin/env python

from my_module import main

if __name__ == '__main__':
    main()

Running the above script gets:

$ ./run_my_module.py
my_module.MyClass

Is there a way I could write the get_full_class_name() function such that it always returns my_module.MyClass regardless of whether my_module is being run as a script?

SuperElectric
  • 13,992
  • 8
  • 43
  • 63

2 Answers2

0

I propose handling the case __name__ == '__main__' using the techniques discussed in Find Path to File Being Run. This results in this new my_utils:

import sys
import os.path

def get_full_class_name(instance):
    if instance.__class__.__module__ == '__main__':
        return '.'.join([os.path.basename(sys.argv[0]),
            instance.__class__.__name__])
    else:
        return '.'.join([instance.__class__.__module__,
            instance.__class__.__name__])

This does not handle interactive sessions and other special cases (like reading from stdin). For this you may have to include techniques like discussed in detect python running interactively.

Community
  • 1
  • 1
mkiever
  • 824
  • 5
  • 12
0

Following mkiever's answer, I ended up changing get_full_class_name() to what you see below.

If instance.__class__.__module__ is __main__, it doesn't use that as the module path. Instead, it uses the relative path from sys.argv[0] to the closest directory in sys.path.

One problem is that sys.path always includes the directory of sys.argv[0] itself, so this relative path ends up being just the filename part of sys.argv[0]. As a quick hack-around, the code below assumes that the sys.argv[0] directory is always the first element of sys.path, and disregards it. This seems unsafe, but safer options are too tedious for my personal code for now.

Any better solutions/suggestions would be greatly appreciated.

import os
import sys
from nose.tools import assert_equal, assert_not_equal

def get_full_class_name(instance):
    '''
    Returns the fully-qualified class name.

    Handles the case where a class is declared in the currently-running script
    (where instance.__class__.__module__ would be set to '__main__').
    '''

    def get_module_name(instance):

        def get_path_relative_to_python_path(path):
            path = os.path.abspath(path)
            python_paths = [os.path.abspath(p) for p in sys.path]
            assert_equal(python_paths[0],
                         os.path.split(os.path.abspath(sys.argv[0]))[0])
            python_paths = python_paths[1:]

            min_relpath_length = len(path)
            result = None
            for python_path in python_paths:
                relpath = os.path.relpath(path, python_path)
                if len(relpath) < min_relpath_length:
                    min_relpath_length = len(relpath)
                    result = os.path.join(os.path.split(python_path)[-1],
                                          relpath)

            if result is None:
                raise ValueError("Path {} doesn't seem to be in the "
                                 "PYTHONPATH.".format(path))
            else:
                return result

        if instance.__class__.__module__ == '__main__':
            script_path = os.path.abspath(sys.argv[0])
            relative_path = get_path_relative_to_python_path(script_path)
            relative_path = relative_path.split(os.sep)

            assert_not_equal(relative_path[0], '')
            assert_equal(os.path.splitext(relative_path[-1])[1], '.py')
            return '.'.join(relative_path[1:-1])
        else:
            return instance.__class__.__module__

    module_name = get_module_name(instance)
    return '.'.join([module_name, instance.__class__.__name__])
SuperElectric
  • 13,992
  • 8
  • 43
  • 63