Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect if class was defined declarative or functional - possible?

Here's a simple class created declaratively:

class Person:
    def say_hello(self):
        print("hello")

And here's a similar class, but it was defined by invoking the metaclass manually:

def say_hello(self):
    print("sayolala")

say_hello.__qualname__ = 'Person.say_hello'

TalentedPerson = type('Person', (), {'say_hello': say_hello})

I'm interested to know whether they are indistinguishable. Is it possible to detect such a difference from the class object itself?

>>> def was_defined_declaratively(cls):
...     # dragons
...
>>> was_defined_declaratively(Person)
True
>>> was_defined_declaratively(TalentedPerson)
False
like image 338
wim Avatar asked Nov 05 '18 16:11

wim


1 Answers

This should not matter, at all. Even if we dig for more attributes that differ, it should be possible to inject these attributes into the dynamically created class.

Now, even without the source file around (from which, things like inspect.getsource can make their way, but see below), class body statements should have a corresponding "code" object that is run at some point. The dynamically created class won't have a code body (but if instead of calling type(...) you call types.new_class you can have a custom code object for the dynamic class as well - so, as for my first statement: it should be possible to render both classes indistinguishable.

As for locating the code object without relying on the source file (which, other than by inspect.getsource can be reached through a method's .__code__ attibute which anotates co_filename and co_fistlineno (I suppose one would have to parse the file and locate the class statement above the co_firstlineno then)

And yes, there it is: given a module, you can use module.__loader__.get_code('full.path.tomodule') - this will return a code_object. This object has a co_consts attribute which is a sequence with all constants compiled in that module - among those are the code objects for the class bodies themselves. And these, have the line number, and code objects for the nested declared methods as well.

So, a naive implementation could be:

import sys, types

def was_defined_declarative(cls):
    module_name = cls.__module__
    module = sys.modules[module_name]
    module_code = module.__loader__.get_code(module_name)
    return any(
        code_obj.co_name == cls.__name__ 
        for code_obj in module_code.co_consts 
        if isinstance(code_obj, types.CodeType)
    )

For simple cases. If you have to check if the class body is inside another function, or nested inside another class body, you have to do a recursive search in all code objects .co_consts attribute in the file> Samething if you find if safer to check for any attributes beyond the cls.__name__ to assert you got the right class.

And again, while this will work for "well behaved" classes, it is possible to dynamically create all these attributes if needed - but that would ultimately require one to replace the code object for a module in sys.__modules__ - it starts to get a little more cumbersome than simply providing a __qualname__ to the methods.

update This version compares all strings defined inside all methods on the candidate class. This will work with the given example classess - more accuracy can be achieved by comparing other class members such as class attributes, and other method attributes such as variable names, and possibly even bytecode. (For some reason, the code object for methods in the module's code object and in the class body are different instances,though code_objects should be imutable) .

I will leave the implementation above, which only compares the class names, as it should be better for understanding what is going on.

def was_defined_declarative(cls):
    module_name = cls.__module__
    module = sys.modules[module_name]
    module_code = module.__loader__.get_code(module_name)
    cls_methods = set(obj for obj in cls.__dict__.values() if isinstance(obj, types.FunctionType))
    cls_meth_strings = [string for method in cls_methods for string in method.__code__.co_consts  if isinstance(string, str)] 

    for candidate_code_obj in module_code.co_consts:
        if not isinstance(candidate_code_obj, types.CodeType):
            continue
        if candidate_code_obj.co_name != cls.__name__:
            continue
        candidate_meth_strings = [string  for method_code in candidate_code_obj.co_consts if isinstance(method_code, types.CodeType) for string in method_code.co_consts if isinstance(string, str)]
        if candidate_meth_strings == cls_meth_strings:
            return True
    return False
like image 54
jsbueno Avatar answered Nov 03 '22 01:11

jsbueno