Closed jaraco closed 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.
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?
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.
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).
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.
Nice! But I also want all of the features of typer (function parameter to argument inference, rich help, and completion support).
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.
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'>
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.
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.
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).
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.
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.
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