jaraco / jaraco.develop

MIT License
5 stars 6 forks source link

Idententify an alternative to autocommand #20

Closed jaraco closed 1 month ago

jaraco commented 1 month ago

In https://github.com/Lucretiel/autocommand/issues/38, we've learned that autocommand is essentially abandoned, with workflow-breaking bugs unable to be fixed, and the maintainer is unwilling to hand off the projects, so the options are to fork the project or identify an alternative.

I've been pleased with typer in the few places I've tried to use it.

I'd like to explore replacing the use of autocommand with typer. If that goes well, that'll be the way to go. Otherwise, we can fork autocommand as coherent-oss/autocommand or maybe do a rewrite of autocommand.

I'd like to use jaraco.develop as the proving ground, as it has a lot of autocommand usage.

/cc @bswck

jaraco commented 1 month ago

Oh, shoot. Reading through the intro docs, it looks like typer doesn't offer one of the basic ergonomic features of autocommand, the ability to decorate a function and have it be the command. It supports invoking a function as a command, but it still requires the __name__ == '__main__' boilerplate.

bswck commented 1 month ago

looks like typer doesn't offer one of the basic ergonomic features of autocommand, the ability to decorate a function and have it be the command

Typer docs focus on running example main functions through typer.run, but there is a possibility of registering custom commands with typer.Typer.command() as a decorator, as in our cli. If that's not it, what do you mean by "the ability to decorate a function and have it be the command" specifically?

jaraco commented 1 month ago

See https://github.com/fastapi/typer/discussions/928 where I describe what I'd like to see.

there is a possibility of registering custom commands with typer.Typer.command() as a decorator,

Right, but that creates a subcommand. I'd like to decorate and be the command. There's no way to decorate main without it becoming {script} main, as far as I can tell.

jaraco commented 1 month ago

I'm now realizing that I can probably build that wrapper.

from coherent import main

@main
def main(...):
    ...

And coherent.main could do all of the magic (set up the app, infer the globals, conditionally run).

bswck commented 1 month ago

My friend's project https://github.com/nekitdev/entrypoint does exactly this, I think. And I'm very frustrated that it doesn't just examine function.__module__--there's never any need to check globals, given that attribute.

jaraco commented 1 month ago

Nice! But I also want all of the features of typer (function parameter to argument inference, rich help, and completion support).

bswck commented 1 month ago

There's no way to decorate main without it becoming {script} main, as far as I can tell.

I think you are looking for a simple Typer.command() or Typer.callback(invoke_without_command=True). Yet, FWIW, it still doesn't behave as the autorun feature you've proposed in https://github.com/fastapi/typer/discussions/928.

# foo.py
from typer import Typer

app = Typer()

@app.command()
# or @app.callback(invoke_without_command=True)
def main() -> None:
    print("called")

__name__ == "__main__" and app()

Using both versions of the above snippet in foo.py, python -m foo --help doesn't list main as a command, and python -m foo prints out called.

So, trimming down the boilerplate, you could use:

from typer import run

def main() -> None:
    print("called")

__name__ == "__main__" and run(main)

with run() creating an app on the fly.

But yeah, having the autorun decorator would be more useful.

jaraco commented 1 month ago

In https://github.com/jaraco/jaraco.ui/commit/208a1c2f966ae0dd466238649a5f734212955de3, I've added a main function, and I tried applying it to jaraco.develop.add-github-secret.

diff --git a/jaraco/develop/add-github-secret.py b/jaraco/develop/add-github-secret.py
index 32acfcf..9dac8d0 100644
--- a/jaraco/develop/add-github-secret.py
+++ b/jaraco/develop/add-github-secret.py
@@ -1,8 +1,8 @@
-import autocommand
+from jaraco.ui.main import main

 from . import github

-@autocommand.autocommand(__name__)
+@main
 def run(name, value, project: github.Repo = github.Repo.detect()):
     project.add_secret(name, value)

But when I did, it failed:

 jaraco.develop main ๐Ÿš .tox/py/bin/python -m jaraco.develop.add-github-secret
(traceback)
RuntimeError: Type not yet supported: <class 'jaraco.develop.github.Repo'>
``` โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Traceback (most recent call last) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ in _run_module_as_main:198 โ”‚ โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ locals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ alter_argv = True โ”‚ โ”‚ โ”‚ โ”‚ code = at 0x105023d70, file โ”‚ โ”‚ โ”‚ โ”‚ "/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.pโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ line 1> โ”‚ โ”‚ โ”‚ โ”‚ main_globals = { โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__name__': '__main__', โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__doc__': None, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__package__': 'jaraco.develop', โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__loader__': <_frozen_importlib_external.SourceFileLoader object at โ”‚ โ”‚ โ”‚ โ”‚ 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__spec__': ModuleSpec(name='jaraco.develop.add-github-secret', โ”‚ โ”‚ โ”‚ โ”‚ loader=<_frozen_importlib_external.SourceFileLoader object at 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ origin='/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-sโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__annotations__': {}, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__builtins__': , โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__file__': โ”‚ โ”‚ โ”‚ โ”‚ '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.pโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__cached__': โ”‚ โ”‚ โ”‚ โ”‚ '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/__pycache__/add-gitโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 'main': , โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ... +1 โ”‚ โ”‚ โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ mod_name = 'jaraco.develop.add-github-secret' โ”‚ โ”‚ โ”‚ โ”‚ mod_spec = ModuleSpec(name='jaraco.develop.add-github-secret', โ”‚ โ”‚ โ”‚ โ”‚ loader=<_frozen_importlib_external.SourceFileLoader object at 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ origin='/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-sโ€ฆ โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚ in _run_code:88 โ”‚ โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ locals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ cached = '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/__pycache__/add-gitโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ code = at 0x105023d70, file โ”‚ โ”‚ โ”‚ โ”‚ "/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.pโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ line 1> โ”‚ โ”‚ โ”‚ โ”‚ fname = '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.pโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ init_globals = None โ”‚ โ”‚ โ”‚ โ”‚ loader = <_frozen_importlib_external.SourceFileLoader object at 0x105189400> โ”‚ โ”‚ โ”‚ โ”‚ mod_name = '__main__' โ”‚ โ”‚ โ”‚ โ”‚ mod_spec = ModuleSpec(name='jaraco.develop.add-github-secret', โ”‚ โ”‚ โ”‚ โ”‚ loader=<_frozen_importlib_external.SourceFileLoader object at 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ origin='/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-sโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ pkg_name = 'jaraco.develop' โ”‚ โ”‚ โ”‚ โ”‚ run_globals = { โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__name__': '__main__', โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__doc__': None, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__package__': 'jaraco.develop', โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__loader__': <_frozen_importlib_external.SourceFileLoader object at โ”‚ โ”‚ โ”‚ โ”‚ 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__spec__': ModuleSpec(name='jaraco.develop.add-github-secret', โ”‚ โ”‚ โ”‚ โ”‚ loader=<_frozen_importlib_external.SourceFileLoader object at 0x105189400>, โ”‚ โ”‚ โ”‚ โ”‚ origin='/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-sโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__annotations__': {}, โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__builtins__': , โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__file__': โ”‚ โ”‚ โ”‚ โ”‚ '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.pโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ '__cached__': โ”‚ โ”‚ โ”‚ โ”‚ '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/__pycache__/add-gitโ€ฆ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ 'main': , โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ ... +1 โ”‚ โ”‚ โ”‚ โ”‚ } โ”‚ โ”‚ โ”‚ โ”‚ script_name = None โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.py:6 in โ”‚ โ”‚ โ”‚ โ”‚ 3 from . import github โ”‚ โ”‚ 4 โ”‚ โ”‚ 5 โ”‚ โ”‚ โฑ 6 @main โ”‚ โ”‚ 7 def run(name, value, project: github.Repo = github.Repo.detect()): โ”‚ โ”‚ 8 โ”‚ project.add_secret(name, value) โ”‚ โ”‚ 9 โ”‚ โ”‚ โ”‚ โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ locals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ github = โ”‚ โ”‚ โ”‚ โ”‚ main = โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.ui/jaraco/ui/main.py:8 in main โ”‚ โ”‚ โ”‚ โ”‚ 5 def main(func, app=typer.Typer(add_completion=False)): โ”‚ โ”‚ 6 โ”‚ if named.get_module(func) == '__main__': โ”‚ โ”‚ 7 โ”‚ โ”‚ app.command()(func) โ”‚ โ”‚ โฑ 8 โ”‚ โ”‚ app() โ”‚ โ”‚ 9 โ”‚ return func โ”‚ โ”‚ 10 โ”‚ โ”‚ โ”‚ โ”‚ โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ locals โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ app = โ”‚ โ”‚ โ”‚ โ”‚ func = โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:326 โ”‚ โ”‚ in __call__ โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:309 โ”‚ โ”‚ in __call__ โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:362 โ”‚ โ”‚ in get_command โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:579 โ”‚ โ”‚ in get_command_from_info โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:555 โ”‚ โ”‚ in get_params_convertors_ctx_param_name_from_function โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:859 โ”‚ โ”‚ in get_click_param โ”‚ โ”‚ โ”‚ โ”‚ /Users/jaraco/code/jaraco/jaraco.develop/.tox/py/lib/python3.12/site-packages/typer/main.py:788 โ”‚ โ”‚ in get_click_type โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ```

It seems that unlike autocommand, typer can't use arbitrary classes in the annotations to construct an object from the argument.

jaraco commented 1 month ago

I see there is support for custom types, which uses Annotated, so maybe that's better. I just hope I don't have to explicitly create a "parser" for a type that's capable of constructing itself from a string.

jaraco commented 1 month ago

Ugh. And it's ugly - if you want to supply a custom parser class, you have to overspecify whether it's an argument or an option (you lose the inference of Argument or Option based on whether a default has been supplied).

jaraco commented 1 month ago

It does seem that this works:

diff --git a/jaraco/develop/add-github-secret.py b/jaraco/develop/add-github-secret.py
index 32acfcf..06b3f7c 100644
--- a/jaraco/develop/add-github-secret.py
+++ b/jaraco/develop/add-github-secret.py
@@ -1,8 +1,17 @@
-import autocommand
+from typing import Annotated
+
+import typer
+from jaraco.ui.main import main

 from . import github

-@autocommand.autocommand(__name__)
-def run(name, value, project: github.Repo = github.Repo.detect()):
+@main
+def run(
+    name,
+    value,
+    project: Annotated[
+        github.Repo, typer.Option(parser=github.Repo)
+    ] = github.Repo.detect(),
+):
     project.add_secret(name, value)

I wonder if it's possible to have main convert the type annotation so that the simpler syntax also works.

jaraco commented 1 month ago

Another feature that autocommand has was the ability to automatically create long and short switches. Typer will only infer long names, and if you want to specify the short name, you have to supply the long name as well :(. Reported.