0

I often find myself wanting to test some function by printing it as the code is run. I like to use f-strings to make the output more descriptive. Something like this:

foo = 2
bar = 5
print(f'foo * bar = {foo * bar}')

which outputs foo * bar = 10. This is nice, but it would be really convenient to make a module that I can always import that does this for me automatically. Something like this:

eval_print.py

def eval_print(function_as_str):
    print(f'{function_as_str} = {eval(function_as_str)}')

if __name__ == '__main__':
    eval_print('2 + 2')

This works great until I actually import it. I ran into the problem here:

celsius.py:

from eval_print import eval_print

class Celsius:
    def __init__(self, temperature = 0):
        self.temperature = temperature

    def to_fahrenheit(self):
        return (self.temperature * 1.8) + 32

man = Celsius()

man.temperature = 37


eval_print('man.temperature')
eval_print('man.to_fahrenheit()')

Running celsius.py gives me a NameError exception because eval_print doesn't have access to celsius.py.

Is there any way to import the module that imports a module? In this case I would like to automatically import celsius.py or whatever other module might import it to eval_print.py.

Community
  • 1
  • 1
ChrisCrossCrash
  • 651
  • 8
  • 18
  • I think you might have an [XY problem](https://en.wikipedia.org/wiki/XY_problem) but I'm not sure. What is the actual problem you are trying to solve? Because it probably does not actually require eval. If you want to run eval on some arbitrary string you need to pass it an appropriate *namespace* through the optional `globals` and `locals` arguments, per https://docs.python.org/3/library/functions.html#eval But I think the more important question is what it is you're trying to do in the first place. – Iguananaut Dec 22 '19 at 16:53
  • Nevermind, I get it. You want to echo some expressions along with their results--I should have read more carefully. – Iguananaut Dec 22 '19 at 16:54
  • My opinion is that if your goal is debugging, use `breakpoint()`, followed by `interact` inside `pdb` for inspection. Maybe some carefully placed `print`s. But if your goal is testing, write unit tests using `unittest`. They will be automatic and repeatable. – progmatico Dec 22 '19 at 17:04
  • 1
    If you want to just log function calls, see my answer but also fix your title. Your title equals to "How do I make a circular import" and you never want to do that. – progmatico Dec 22 '19 at 19:22
  • @Iguananaut thanks for pointing me in the right direction. That's exactly what I was looking for! – ChrisCrossCrash Dec 22 '19 at 22:11
  • @progmatico, thank for the advice. I haven't dug into debugging too much, but your recommendations are certainly on the top of my list. I'll check them out very soon :) – ChrisCrossCrash Dec 22 '19 at 22:11

2 Answers2

2

You can write a function call log decorator like this, (somewhat inspired by blueteeth answer too) and then decorate the functions you want to inspect the calls.

def log_call(func):
    def print_call(*args, **kwargs):
        result = func(*args, **kwargs)
        args_str = ", ".join(str(arg) for arg in args)
        kwargs_str = " " + ", ".join(
            f"{key}={value}".format(key, value) for key, value in kwargs.items()
        )
        print(f"{func.__name__}({args_str},{kwargs_str}) = {result}")
        return result

    return print_call


@log_call
def add(x, y, offset=0, from_origin=0):
    return x + y + offset + from_origin


print(add(1, 2, offset=10, from_origin=5))

Outputs:

add(1, 2, offset=10, from_origin=5) = 18
18
progmatico
  • 4,714
  • 1
  • 16
  • 27
1

Here's a way to do it for functions. This is only written for positional arguments but you could adapt it for named arguments.

def printe(fun, args):
    sargs = ', '.join([str(arg) for arg in args])
    print(f'{fun.__name__}({sargs}) = {fun(*args)}')

When given the actual function and the arguments as a tuple it prints it out.

>>> printe(foo, (1,))
foo(1) = 2

Problem is that it doesn't work if you assign a function to another variable.

>>> bar = foo
>>> printe(bar, (1,))
foo(1) = 2

Not sure how to do variables, but I'll keep thinking.

blueteeth
  • 3,330
  • 1
  • 13
  • 23