rec / def_main

🗣 A decorator for main 🗣
0 stars 0 forks source link

always returns 0 #1

Open ChrisIdema opened 2 weeks ago

ChrisIdema commented 2 weeks ago

header.py:

import def_main
import sys

@def_main
def main(*argv):
    # sys.exit(0) # returns 0
    # sys.exit(1) # returns 1
    # exit(0) # returns 0
    # exit(1) # returns 1
    #return 0 # returns 0
    return 1 # returns 0, should be 1!

I run in powershell: python .\header.py; echo $LastExitCode

this does work:

def main(*argv):
    return 1

if __name__ == '__main__':
    import sys

    returncode = main(sys.argv[1:])
    sys.exit(returncode)

Your documentation says they should be equivalent.

also, why *argv and not 'argv'? It just puts the list in a tuple.

rec commented 2 weeks ago

Heya!

Both of these are excellent questions. The cause is that when I tried out the first version of this on reddit's r/python, the response was so negative that I didn't go on with it!

I'm in transit today, but I'll look at it tomorrow AM...

Thanks for finding these!

ChrisIdema commented 2 weeks ago

Yeah I saw those comments. I disagree with them. I love python, but this always seem non-pythonic to me. More like a hack. Fine for early versions of python, but they should have had a substitute by now.

rec commented 2 weeks ago

All right, I fixed the error code issue!

Remember, there might be multiple def_main blocks, so what I do is call sys.exit the first time that any of them returns a truthy value, or otherwise, fall off the bottom.

I'm not quite sure about passing *sys.argv[1:] without the *. I have an example of what I'm thinking in test_main.py, take a look and tell me what you think.

ChrisIdema commented 2 weeks ago

One thing I don't understand is the reasoning behind allowing multiple mains. It kind of defeats the purpose of a main function in my opinion. If imported as a module it should not run the main function(this works), otherwise it should run it, accept parameters (just like in c) and return a value to indicate success or an error code. If it is successful it doesn't make sense to me to run another main. I would just run the first main function defined with the decorator without the fall through and always return. The user can always call other functions.

rec commented 2 weeks ago

No, no, it doesn't run anything if it's imported as a module, what would be the point, as you said? 😁

This is what happens if you decorate more than one function in the same module with def_main and then that specific module is run as main

Example: foo.py:

@def_main
def one(*argv):
    print('one', *argv)

@def_main
def two(*argv):
    print('two', *argv)

And then you call python foo.py. Any functions decorated with def_main in other modules don't get run...


There seem to be three choices, if one doesn't fail: I can call two, I can do nothing, or raise an exception. The first one is both the easiest, and the most intuitive, I think.

I don't expect people to actually do that very often, but people will do it at some point and something clear needs to happen.

ChrisIdema commented 2 weeks ago

No, no, it doesn't run anything if it's imported as a module, what would be the point, as you said? 😁

I know. That part works. I was just being complete in specifying what behavior I expect for def_main functions: Included as module: don't run main(this works). Run directly: only run first @def_main function like in C (this only works in case of failure).

There seem to be three choices, if one doesn't fail: I can call two, I can do nothing, or raise an exception. The first one is both the easiest, and the most intuitive, I think.

Just to clarify by fail do you mean return non-zero/raise an exception?

Calling two() on success is what I find illogical. The user can always explicitly call two() on their own terms. But just like in C I expect returning from main ends the program. Allowing multiple decorators is fine. The top one will always be prioritized. The user can uncomment the top function or just the decorator of the top function to run the bottom one.

With "do nothing" you mean exit with the return code? No return means none is returned and sys.exit() automatically returns 0 in case of none (https://docs.python.org/2/library/sys.html#sys.exit). That is what I expect for a main function. Just like in C. This is what I propose.

To me your option 3 doesn't make sense. Why raise an exception on success?

In case of returning non-zero I would simply exit with this value. And this works.

In my opinion this is enough:

def def_main(f: Callable) -> Callable:
    s = inspect.stack()[1]
    MAINS.append((f, s.filename, s.lineno))

    if f.__module__ == '__main__':
        sys.exit(f(*sys.argv[1:]))
    return f
rec commented 1 week ago

I'm not really fond of invisibly failing to do things, though! If someone did use the decoration twice, I'd either want to call both functions, or report an error.

Let me think about this further, but for the moment, I'm going to leave it as-is. If you only use it one time, there won't be a difference.

Now, what about *argv vs argv?

ChrisIdema commented 1 week ago

The asterisk puts it in a tuple. So you have a list in a tuple. I don't see the benefit of that. Also I don't see the benefit in skipping the first argument (filename).