jorenham / mainpy

Simplify your project's main entrypoint definition with @main
https://pypi.org/project/mainpy/
MIT License
6 stars 2 forks source link

Deterministic `@main` return type #46

Closed jorenham closed 1 month ago

jorenham commented 1 month ago

Currently, the @main control flow is roughly as follows:

I.E. the mainpy.main return type depends on the (untype-able) context. This is rather horrible for type-checkers, because they can't infer what the concrete return type.

So I propose the following solution:

Always return a MainFunction[RT] instance (suggestions for a better name are welcome) from mainpy.main. It's a lightweight callable wrapper around the __wrapped__ function patched with functools.update_wrapper` of course are we taking notes, Click?, with several additional attributes:

The MainResult[RT] type is a NamedTuple with the following attributes:

(perhaps more attributes could be added later, e.g. a copy of sys.argv, or a runtime: timdelta or something)

The MainOptions is a TypedDict representing the mainpy.main keyword-only parameters.

I don't see any need for explicit exception handling, since any raised exception should just propagate.

jorenham commented 1 month ago

@KotlinIsland Any thoughts on this?

KotlinIsland commented 1 month ago

what's the use case of calling main when it's not main?

so it will return a callable object that might already contain a result?

i would think that it should expect an int to be returned, and call sys.exit(value) with it. but maybe that's quite simple to do manually

can we invent a new type system concept to represent the semantics of __main__? would that help at all?

jorenham commented 1 month ago

Yea so currently with

import time
import mainpy

@mainpy.main
def app() -> int:
    return time.perf_counter_ns()

you'll have app: int if app.__module__ == '__main__', and app: () -> float otherwise.

Apart from that being awkward to type, it's also something like a convention that a function decorator returns a callable, which in this case, isn't (always) the case.


what's the use case of calling main when it's not main?

Perhaps letting typer or something do it for you, e.g. in case you have @main def app(): ... in __init__.py and also a __main__.py.

Or in case of sub-packages, where you could python -m zoo.sloth --tickle but also python -m zoo sloth --tickle (I've built something similar with typer once , but the sloth sued me and now I have a restraining order).

so it will return a callable object that might already contain a result?

Yea that's basically my proposal.

Although I doubt that there's a situation where anyone would (or even could) look at the result though 🤷🏻. So I suppose it's just ~an over-engineered~ a fancy solution to an awkward typing situation, as opposed to a practical one.

i would think that it should expect an int to be returned, and call sys.exit(value) with it. but maybe that's quite simple to do manually

Yea I considered doing that before, but then got scared by all the OS-dependent stuff about return codes. Plus, I'm not sure if it'll still work with e.g. click and typer that way 🤷🏻

jorenham commented 1 month ago

can we invent a new type system concept to represent the semantics of __main__?

How about optype.HasModule[Literal['__main__']]? https://github.com/jorenham/optype/blob/1228112deef5dbbd590b1207173f5d7cd41e9159/optype/_has.py#L81-L83

would that help at all?

🤷🏻‍♂️