17

If a file myfile.py contains:

class A(object):
  # Some implementation

class B (object):
  # Some implementation

How can I define a method so that, given myfile.py, it returns [A, B]?

Here, the returned values for A and B can be either the name of the classes or the type of the classes.

(i.e. type(A) = type(str) or type(A) = type(type))

Eric McLachlan
  • 3,132
  • 2
  • 25
  • 37
  • `dir(my file)`? – Daniel Roseman Mar 08 '19 at 16:29
  • What are you intending to do with those classes? Does it matter that they are *classes* or is any *callable* sufficient? Do you want all the names in the module that name classes, or just the classes that were *defined* within that file? – Daniel Pryden Mar 08 '19 at 16:32
  • @DanielPryden: I want to eventually create instances of those types dynamically. So, I would prefer classes to callables. I prefer not to have all the names in the module. Rather, I would like only those classes defined in the file or a way to easilly screen all members to return only hose classes defined in the file. – Eric McLachlan Mar 08 '19 at 17:40
  • 1
    @EricMcLachlan: I suspect you would be better served by something like a class decorator that registers classes that can be dynamically instantiated with a central registry. Basically: you probably want this to be opt-in rather than a heuristic that opts some classes out. – Daniel Pryden Mar 08 '19 at 17:42
  • Also: you probably don't want to discover all classes in a module, since there is nothing meaningful you can do with a class object if you don't know anything about it *a priori* -- you can't even construct an instance unless you know the correct constructor arguments to provide. And if your ultimate goal is to provide a way for clients to construct arbitrary objects you have a gaping security hole which will be impossible to completely close with a blacklisting approach: you will *need* to use whitelisting to be secure. – Daniel Pryden Mar 08 '19 at 17:45
  • @DanielPryden: No doubt you are right that a decorator would work better. As someone recently said to me, "Python isn't my first language". :P I've added my use-case below. It works. That's good enough for me for now. Thanks again! – Eric McLachlan Mar 08 '19 at 18:16

5 Answers5

31

You can get both:

import importlib, inspect
for name, cls in inspect.getmembers(importlib.import_module("myfile"), inspect.isclass):

you may additionally want to check:

if cls.__module__ == 'myfile'
Sebastian Nielsen
  • 3,835
  • 5
  • 27
  • 43
panda-34
  • 4,089
  • 20
  • 25
8

In case it helps someone else. Here is the final solution that I used. This method returns all classes defined in a particular package.

I keep all of the subclasses of X in a particular folder (package) and then, using this method, I can load all the subclasses of X, even if they haven't been imported yet. (If they haven't been imported yet, they cannot be accessible via __all__; otherwise things would have been much easier).

import importlib, os, inspect

def get_modules_in_package(package_name: str):
    files = os.listdir(package_name)
    for file in files:
        if file not in ['__init__.py', '__pycache__']:
            if file[-3:] != '.py':
                continue

            file_name = file[:-3]
            module_name = package_name + '.' + file_name
            for name, cls in inspect.getmembers(importlib.import_module(module_name), inspect.isclass):
                if cls.__module__ == module_name:
                    yield cls
Eric McLachlan
  • 3,132
  • 2
  • 25
  • 37
2

It's a bit long-winded, but you first need to load the file as a module, then inspect its methods to see which are classes:

import inspect
import importlib.util

# Load the module from file
spec = importlib.util.spec_from_file_location("foo", "foo.py")
foo = importlib.util.module_from_spec(spec)
spec.loader.exec_module(foo)

# Return a list of all attributes of foo which are classes
[x for x in dir(foo) if inspect.isclass(getattr(foo, x))]
match
  • 10,388
  • 3
  • 23
  • 41
  • 2
    Note that if you `import` a class from another module, it will show up in the result here, even though it wasn't declared within this module. For example, if you have `from datetime import timedelta` at the top of the module, `timedelta` will show up as an attribute of the module object and `inspect.isclass` will correctly identify it was a class. – Daniel Pryden Mar 08 '19 at 16:41
  • Nothing at all wrong with this answer. I just happened to get the other solution working first. Thanks for your input! – Eric McLachlan Mar 08 '19 at 18:17
0

Just building on the answers above.

If you need a list of the classes defined within the module (file), i.e. not just those present in the module namespace, and you want the list within that module, i.e. using reflection, then the below will work under both __name__ == __main__ and __name__ == <module> cases.

import sys, inspect

# You can pass a lambda function as the predicate for getmembers()
[name, cls in inspect.getmembers(sys.modules[__name__], lambda x: inspect.isclass(x) and (x.__module__ == __name__))]

In my very specific use case of registering classes to a calling framework, I used as follows:

def register():    
    myLogger.info(f'Registering classes defined in module {__name__}')
    for name, cls in inspect.getmembers(sys.modules[__name__], lambda x: inspect.isclass(x) and (x.__module__ == __name__)):
        myLogger.debug(f'Registering class {cls} with name {name}')
        <framework>.register_class(cls)
0

In case importing the file throws errors or is not desirable, one could use the following:

import pyclbr
[k for k, v in pyclbr.readmodule('myfile.py').items() if isinstance(v, pyclbr.Class)]
User2321
  • 2,952
  • 23
  • 46