pythonclasspropertiesdecoratorabstract-syntax-tree

unexpected behavior with `inspect.getmembers` on @property methods that throw exceptions


I feel like I'm encountering a rather strange behavior in Python. Try it out yourself:

import inspect

class SomeClass:
    def __init__(self):
        inspect.getmembers(self, predicate=inspect.ismethod)

    def this_is_okay(self):
        raise Exception('an error that may be thrown')

    @property
    def this_is_not_okay(self):
        raise Exception('an error that may be thrown')

    @property
    def but_this_is_okay(self):
        if True:
            raise Exception('an error that may be thrown')

Inspecting the methods of a class will cause an error if there is a method decorated with @property, but only if it throws an error at the first indentation level.

How can this be? And how can I get around this?

P.S. The reason I'm inspecting like so is I'm trying to get an array of the class methods (actual callable objects) in the order that they're defined in the class.


Solution

  • But you aren't actually calling inspect.getmembers on the class, as self refers to an instance of a class. The error will happen as it will attempt to use the getattr getter (which you can infer from the Traceback), and given that SomeClass().this_is_not_okay will effectively raise an exception, that will break inspect.getmembers. This is in fact a known issue and has been open since 2018. Also, your assertion that "but only if it throws an error at the first indentation level" is not true, because inspect.getmembers simply aborted with an exception earlier at this_is_not_okay (though changing the condition to if False will make it work as it will stop the execution of the raise statement).

    Now, you described your stated goal is to get the order that things are defined in the class, well, you have to pass it the class. Example:

    import inspect
    
    class SomeClass:
        def this_is_okay(self):
            raise Exception('an error that may be thrown')
    
        @property
        def this_is_not_okay(self):
            raise Exception('an error that may be thrown')
    
        @property
        def but_this_is_okay(self):
            if False:
                raise Exception('an error that may be thrown')
    
    some_ins = SomeClass()
    print(inspect.getmembers(type(some_ins), predicate=inspect.isfunction))
    

    The above should produce something like:

    [('this_is_okay', <function SomeClass.this_is_okay at 0x7f166ae1a980>)]
    

    If using Python 3.11 or later, inspect.getmembers_static may be used instead:

    print(inspect.getmembers_static(some_ins, predicate=inspect.isfunction))
    

    Which does effectively the same thing.

    Note that I changed the predicate to inspect.isfunction as class definitions don't have methods in general, but those functions generally will become functions at the class instance, so the distinction should be minimal.

    If you must work using the inspect.ismethod predicate, you may have to get every member for the class instance, and then apply the filtering you want.