Why do script entrypoints require a function be specified?

The entrypoints spec describes entrypoints as so:

The object reference points to a Python object. It is either in the form importable.module , or importable.module:object.attr.

where for scripts

My question is: why must a function be given, rather than just executing a module/package?

I have an entrypoint that looks like minegauler = minegauler.app.__main__:main, which is normally run with python -m minegauler.app - why can’t I just specify minegauler = minegauler.app?

I’m assuming this case could be handled fairly easily using something like the following?

import runpy
runpy.run_module("minegauler.app", run_name="__main__")

Just for others’ reference, this concern was originally raised on the PEP 621 Setuptools testing thread, and discussed at some length there, which readers here might want to peruse first, particularly @abravalheri 's detailed response giving some additional background on the current situation.

In any case, at least to my naive analysis, it would seem quite possible and desirable, either via a specification update PR or a short PEP (which I’m happy to help with), to modify this section to allow the behavior implied above (i.e. that of runpy)—specifically, if a module is specified, execute it as a script with __name__ == "__main__", and if a package, execute __main__,py inside of it. This would simplify and, thusly, encourage a consistent best-practice approach to declaring a package’s main entrypoint, if it has one; namely, for package spam,

[project.scripts]
spam-cli = "spam"

instead of the following,

[project.scripts]
spam-cli = "spam.__main__.main"

for an entry point that would work equally by executing it via spam-cli and via python -m spam.

@pf_moore what do you think about this?

5 Likes

You might need to edit that post to move the last bit of text out of the preformatted block :slight_smile:

1 Like

Oops, thanks! Not sure how I missed seeing that; I usually carefully check the preview and/or the posted output for such mistakes.

Pinging @pf_moore again in case he didn’t see it before due to that.

Not sure why you’re asking me in particular, but I have no real opinion on the matter here. Allowing a module seems reasonable, given that it’s allowable for other types of entry point, but I’ve no idea what the trade-offs are.

The script usage was, as far as I’m aware, originated by setuptools, so I’d look into the history of that feature to see if there’s any indication of why it doesn’t handle modules (maybe it’s just because runpy wasn’t available then?)

1 Like

Because you’re the package metadata czar who would presumably be PEP-Delegate for any such PEP on this, and it would involve a change to several of the various specs you currently administer, alongside your role with pip :smile:

Do we need a PEP for that?

The text in the living standard is not explicit about disallowing the :obj.attr part of an entry-point string from being omitted in this scenario.
Moreover, in the end of the day, this change would be backward-compatible right?

If someone is interested in pushing this behaviour forward, I think they could do:

  1. Coordinate with pip and distlib maintainers if they are supportive of this change.
  2. Create a PR in distlib implementing this new behaviour (I am assuming pip uses distlib to creating the script wrappers)
  3. Wait until this is released in distlib and vendored back into pip
  4. Submit a “clarification” PR to the living standard.
2 Likes

Precisely this, although whoever wants to do this should also co-ordinate with, and create a PR for, installer. And I can confirm that pip just calls distlib here, so there would be no pip change needed (apart from maybe adding a test to ensure that the new-style script specifications work as intended).

Personally, I think this seems like it would be a fair bit of work for a relatively minor benefit, so I wonder whether it’s really a good use of the community’s time? :person_shrugging:

2 Likes

Thanks; that’s the other main reason I was calling upon you in that capacity, which I meant to go back here and clarify, was to your take on whether this should just be a spec update, or require a PEP.

It’s certainly non-trivial, but while the benefit is not huge, neither seems to be the work, so long as there are a few motivated people to share the load. The clarification PR would be a cinch for me, and I could try to help with the implementation, given some pointers as to what code would need to be modified and any specific guidance as to how, and perhaps @LewisGaul could too?

@vsajip / @vsajip1 and @pradyunsg , what are your thoughts on this?

I’m happy to help out with the implementation/testing/docs if someone can point in the right direction, it might be a nice way for me to gain some familiarity with the packaging projects.

I can understand the perspective that it’s only a small gain, but from a user’s perspective this just seems like an obvious “why isn’t this supported” quality of life thing, so I’m definitely in favour of it being done.

2 Likes

This discussion looks like it was really going somewhere … did it? I still seem to need abc.__main__:main as an entry point, which requires an if __name__ == "__main__" in __main__.py. This seems like an obvious artifact of previous “best practices” that would mainly be nice to tidy up. Where else can I throw encouragement?

That doesn’t seem accurate. We just have two mecanisms for the script need.

A module with a block if __name__ == '__main__': provides a way to execute it as a script, either because the file is installed as a script or it’s a module and people invoke it with python3 -m mod. But if it’s only installed as a script, or is a __main__ module in a package, then the check can be omitted.
[edit: this is indeed recommended in the docs: __main__ — Top-level code environment — Python 3.12.2 documentation ]

Entry-points are a more complicated system, with executable wrapping on windows, that require a function definition but no __name__ check.

If you install the resulting package into a venv, you’ll find a
separate entrypoint script in its bin subdirectory which can be
executed directly to call the function identified in your entrypoint
configuration. That is independent of and does not require the name
conditional in your .py file.