LearnIT
LearnIT

Reputation: 346

How do I call globals() from another imported function in Python?

I am currently developing an automated function tester in Python.

The purpose of this application is to automatically test if functions are returning an expected return type based on their defined hints.

Currently I have two test functions (one which fails and one which passes), along with the rest of my code in one file. My code utilizes the globals() command in order to scan the Python file for all existing functions and to isolate user-made functions and exclude the default ones.

This initial iteration works well. Now I am trying to import the function and use it from another .py file.

When I run it in the other .py file it still returns results for the functions from the original file instead of the new test-cases in the new file.

Original File - The Main Application


from math import floor
import random

#declaring test variables
test_string = 'test_string'
test_float = float(random.random() * 10)
test_int = int(floor(random.random() * 10))

#Currently supported test types (input and return)
supported_types = ['int', 'float', 'str']

autotest_result = {}


def int_ret(number: int) -> str:
    string = "cactusmonster"
    return string

def false_test(number: int) -> str:
    floating = 3.2222
    return floating

def test_typematching():
    for name in list(globals()):
        if not name.startswith('__'):
            try:
                return_type = str((globals()[name].__annotations__)['return'])
                autotest_result.update({name: return_type.replace("<class '", "").replace("'>", "")})
            except:
                continue
    for func in autotest_result:
        if autotest_result[func] != None:
            this_func = globals()[func].__annotations__
            for arg in this_func:
                if arg != 'return':
                    input_type = str(this_func[arg]).replace("<class '", "").replace("'>", "")
                    for available in supported_types:
                        if available == input_type:
                            func_return = globals()[func]("test_" + input_type)
                            func_return = globals()[func]("test_" + input_type)
                            actual_return_type = str(type(func_return)).replace("<class '", "").replace("'>", "")
                            if actual_return_type == autotest_result[func]:
                                autotest_result[func] = 'Passed'
                            else:
                                autotest_result[func] = 'Failed'
    return autotest_result


Test File - Where I Am Importing The "test_typematching()" Function

from auto_test import test_typematching

print(test_typematching())
def int_ret_newfile(number: int) -> str:
    string="cactusmonster"
    # print(string)
    # return type(number)
    return string

Regardless if I run my main "auto_test.py" file or the "tester.py" file, I still get the following output: {'int_ret': 'Passed', 'false_test': 'Failed'}

I am guessing this means that even when I am running the function from auto_test.py on my tester.py file it still just scans itself. I would like it to scan the file where the function is currently being called. For example, I expect it to test the int_ret_newfile function of tester.py.

Any advice or help would be much appreciated.

Upvotes: 2

Views: 818

Answers (1)

gilch
gilch

Reputation: 11681

globals() is a bit of a misnomer. It gets the calling module's __dict__. (Python's true "global" namespace is actually builtins.)

How can globals() get its caller's __dict__ when it's defined in the builtins module? Here's a clue:

PyObject *
PyEval_GetGlobals(void)
{
    PyThreadState *tstate = _PyThreadState_GET();
    PyFrameObject *current_frame = _PyEval_GetFrame(tstate);
    if (current_frame == NULL) {
        return NULL;
    }

    assert(current_frame->f_globals != NULL);
    return current_frame->f_globals;
}

globals() is one of those builtins that's implemented in C (in CPython), but you get the gist. It reads the frame globals from the current stack frame, so in Python,

import inspect
inspect.currentframe().f_globals

would do the same thing as globals(). But you can't just put this in a function and expect it to work the same way, because calling it would add a stack frame, and that frame's globals depends on the function's .__globals__ attribute, which is set to the .__dict__ of the module that defined it. You want the caller's frame.

def myglobals():
    """Behaves like the builtin globals(), but written in Python!"""
    return inspect.currentframe().f_back.f_globals

You could do the same thing in test_typematching. But walking up the stack to the previous frame like that is a weird thing to do. It can be surprising and brittle. It amounts to passing the caller's frame as an implicit hidden argument, something that normally is not supposed to matter. Consider what happens if you wrap it in a decorator. Now which stack frame are you getting the globals from?

So really, you should be passing in globals() as an explicit argument to test_typematching(), like test_typematching(globals()). A defined and documented parameter would be much less confusing than implicit introspection. "Explicit is better than implicit".

Still, Python's standard library does do this kind of thing occasionally, with globals() itself being a notable example. And exec() can use the current namespace if you don't give it a different one. It's also how super() can now work without arguments in Python 3. So stack frame inspection does have precedent for this kind of use case.

Upvotes: 4

Related Questions