12

The intention was to implement some sort of plugin framework, where plugins are subclasses (i.e. B) of the same base class (i.e. A). The base class is loaded with standard import, whereas subclasses are loaded with imp.load_module() from the path of a well-known package (i.e. pkg).

pkg/
    __init__.py
    mod1.py
        class A
    mod2.py
        class B(pkg.mod1.A)

This worked fine with real subclasses, i.e.,

# test_1.py
import pkg
from pkg import mod1
import imp
tup = imp.find_module('mod2', pkg.__path__)
mod2 = imp.load_module('mod2', tup[0], tup[1], tup[2])
print(issubclass(mod2.B, mod1.A)) # True

But the problem came when testing the base class itself,

# test_2.py
import pkg
from pkg import mod1
import imp
tup = imp.find_module('mod1', pkg.__path__)
mod0 = imp.load_module('mod1', tup[0], tup[1], tup[2])
print(issubclass(mod0.A, mod1.A)) # False

But mod0.A and mod1.A are actually the same class from the same file (pkg/mod1.py).

This issue appears in both python 2.7 and 3.2.

Now the question is two-fold, a) Is it an expected feature or a bug of issubclass(), and b) How to get rid of this without changing contents of pkg?

Cœur
  • 37,241
  • 25
  • 195
  • 267
liuyu
  • 1,279
  • 11
  • 25
  • Why aren't you using simple `import` statements? This would avoid this kind of problem. – Sven Marnach Jul 12 '12 at 22:02
  • Please show the `import` statement in `mod2` that imports `mod1`. It matters whether this statement is `import mod1` or `from pkg import mod1` or `from . import mod1`. – Sven Marnach Jul 12 '12 at 22:04
  • You've imported the module that contains your base class twice and all the objects in the module have also been duplicated. Try `print mod0.A is mod1.A` and see if it isn't `False`. Or even `mod0 is mod1`. – kindall Jul 12 '12 at 22:05
  • @SevenMarnach I am implementing a plugin framework, where the names of subclasses are only available in runtime, and cannot be hard-coded. – liuyu Jul 12 '12 at 22:06
  • @liuyu: There are oodles of Python plugin frameworks out there. If you don't have intimate knowledge of the Python import mechanism, I suggest using one of these frameworks rather than rolling your own one. Anyway, the fact the "names of subclasses are only available in runtime" shouldn't stop you from hard-coding an import statement, right? – Sven Marnach Jul 12 '12 at 22:13
  • @kindall Both (mod0.A is mod1.A) and (mod0 is mod1) in test_2 are False. Two *instances* of the same module imported from different paths are considered different modules (at least true for python 2.7 and python 3.2). Otherwise, there wouldn't be the problem at all. – liuyu Jul 12 '12 at 22:16

3 Answers3

11

They aren't the same class. They were created with the same code, but since you executed that code twice (once in the import and once in load_module) you get two different class objects. issubclass is comparing the identities of the class objects and they're different.

Edit: since you can't rely on issubclass, one possible alternative is to create a unique attribute on the base class that will be inherited by the derived classes. This attribute will exist on copies of the class as well. You can then test for the attribute.

class A:
    isA = True

class B(A):
    pass

class C:
    pass

def isA(aclass):
    try:
        return aclass.isA
    except AttributeError:
        return False

print isA(A)
True
print isA(B)
True
print isA(C)
False
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • 10
    "Doc, it hurts when I do that!" "Don't do that." – kindall Jul 12 '12 at 22:43
  • @liuyu, I haven't been able to test this but you might be able to check if the classes are equal: `issubclass(mod0.A, mod1.A) or mod0.A == mod1.A` – Mark Ransom Jul 12 '12 at 22:47
  • @MarkRansom (mod0.A == mod1.A) evaluates to False. And you are right, they are different classes resulted from the same code executed twice, i.e. id(mod0.A) and id(mod1.A) differ. – liuyu Jul 12 '12 at 23:09
  • @liuyu, added a workaround for you. Sorry I couldn't get to it sooner. – Mark Ransom Jul 13 '12 at 03:41
  • I have a similar case. In file-A I define a base-class, that a class in file-B subclasses. In file-A I then use `imp.load_source` to load the class in file-B. Does your answer apply, specifically _"They aren't the same class"_? – Jeppe Feb 04 '21 at 07:59
  • @Jeppe that's what I found through some debugging. If I took the shared class and moved it to a sub directory of the classes that needed it, I didn't have to use `load_module` or any variant, which meant it was only loaded once across main/sub directory classes. Definitely learned an interesting lesson here. – Skeeterdrums Mar 14 '21 at 15:28
7

Since I spent some time fiddling with this, I thought I would share my solution:

import inspect

...

def inherits_from(child, parent_name):
    if inspect.isclass(child):
        if parent_name in [c.__name__ for c in inspect.getmro(child)[1:]]:
            return True
    return False

print inherits_from(possible_child_class, 'parent_class')
#True

Of course this only really checks that the child class inherits from a class CALLED parent_class, but for my purpose (and most I suspect) that's fine.

Note: this returns false if possible_child_class is an instance of parent_class due to the [1:].

oz123
  • 27,559
  • 27
  • 125
  • 187
SColvin
  • 11,584
  • 6
  • 57
  • 71
1
#!/usr/bin/env python

import os
import sys
import pkg
from pkg import mod1
import imp


def smart_load_module(name, path):
    # get full module path
    full_path = os.path.abspath(os.path.join(path[0], name))

    for module_name, module in sys.modules.items():
        # skip empty modules and ones without actual file
        if not module or not hasattr(module, '__file__'):
            continue

        # remove extension and normalize path
        module_path = os.path.abspath(os.path.splitext(module.__file__)[0])
        if full_path == module_path:
            return module

    # if not found, load standard way
    tup = imp.find_module(name, path)
    return imp.load_module(name, tup[0], tup[1], tup[2])


if __name__ == '__main__':
    mod00 = smart_load_module('mod1', pkg.__path__)
    print(issubclass(mod00.A, mod1.A))  # True

    tup = imp.find_module('mod1', pkg.__path__)
    mod0 = imp.load_module('mod1', tup[0], tup[1], tup[2])
    print(issubclass(mod0.A, mod1.A))  # False

This works for me. I search class by full path in sys.modules and return loaded instance if any found.

grundic
  • 4,641
  • 3
  • 31
  • 47