Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Idententify an alternative to autocommand #20

Closed
jaraco opened this issue Aug 15, 2024 · 12 comments · Fixed by #21
Closed

Idententify an alternative to autocommand #20

jaraco opened this issue Aug 15, 2024 · 12 comments · Fixed by #21
Assignees

Comments

@jaraco
Copy link
Owner

jaraco commented Aug 15, 2024

In Lucretiel/autocommand#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 jaraco self-assigned this Aug 15, 2024
@jaraco
Copy link
Owner Author

jaraco commented Aug 15, 2024

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
Copy link
Contributor

bswck commented Aug 15, 2024

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
Copy link
Owner Author

jaraco commented Aug 15, 2024

See fastapi/typer#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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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
Copy link
Contributor

bswck commented Aug 15, 2024

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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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

@bswck
Copy link
Contributor

bswck commented Aug 15, 2024

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 fastapi/typer#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
Copy link
Owner Author

jaraco commented Aug 15, 2024

In jaraco/jaraco.ui@208a1c2, 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 = <code object <module> 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__': <module 'builtins' (built-in)>,                           │ │
│ │                │   '__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': <function main at 0x1051b0180>,                                   │ │
│ │                │   ... +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 = <code object <module> 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__': <module 'builtins' (built-in)>,                           │ │
│ │                │   '__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': <function main at 0x1051b0180>,                                   │ │
│ │                │   ... +1                                                                    │ │
│ │                }                                                                             │ │
│ │  script_name = None                                                                          │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/add-github-secret.py:6 in <module>       │
│                                                                                                  │
│   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 = <module 'jaraco.develop.github' from                                                │ │
│ │          '/Users/jaraco/code/jaraco/jaraco.develop/jaraco/develop/github.py'>                │ │
│ │   main = <function main at 0x1051b0180>                                                      │ │
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
│                                                                                                  │
│ /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 = <typer.main.Typer object at 0x1051c9370> │                                              │
│ │ func = <function run at 0x1072d3e20>            │                                              │
│ ╰─────────────────────────────────────────────────╯                                              │
│                                                                                                  │
│ /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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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
Copy link
Owner Author

jaraco commented Aug 15, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants