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

[FEATURE] Add easy --version #52

Closed
dbanty opened this issue Feb 28, 2020 · 16 comments
Closed

[FEATURE] Add easy --version #52

dbanty opened this issue Feb 28, 2020 · 16 comments
Labels
feature New feature, enhancement or request

Comments

@dbanty
Copy link

dbanty commented Feb 28, 2020

Is your feature request related to a problem

I'd like a way to do the standard my-command --version to exit an print the version number of the application. Click offers a version_option decorator that you can put on the entrypoint of your application to achieve this relatively easily.

The solution you would like

The ability to pass in a version string to an application and have the option added automatically. I'd then end up doing something like this:

from importlib.metadata import version

app = typer.Typer(version=version(__package__))

Though if it's possible to attempt to extract the package version automatically without having to pass it in, that would be even better.

Describe alternatives you've considered

I attempted to just add a version option in my callback like below, but I can't seem to call this without passing in a command.

@app.callback()
def cli(display_version: bool = typer.Option(False, "--version")) -> None:
    if display_version:
        print(f"Version: {version(__package__)}")
        raise typer.Exit()

However if I just try my-command --version it gives me an error saying I need to pass a command. It would be nice to be able to add custom top level options like --help which are allowed to run. Then users could implement things like --version themselves.

@dbanty dbanty added the feature New feature, enhancement or request label Feb 28, 2020
@dbanty dbanty changed the title [FEATURE] Add easy --version [FEATURE] Add easy --version Feb 28, 2020
@samuelcolvin
Copy link

This would be great.

@euri10
Copy link

euri10 commented Mar 9, 2020

I think both
#41
#42
and this are in fact the very same topic aka how to use / define global options

click has a version_option decorator which is basically syntactic sugar for #41 implementation
I tried and tried and failed a way to use it in typer and failed each time

@tiangolo
Copy link
Member

Hey everyone! Thanks for the discussion!

Yeah, Click decorators won't work with Typer, although you can generate a Click object from a Typer app (there's a new section in the docs about it).

But I just added docs about adding a --version CLI option: https://typer.tiangolo.com/tutorial/options/version/ 🚀

@dbanty dbanty closed this as completed Mar 20, 2020
@kesavkolla
Copy link

How to use version when I'm using app with different sub commands. Is there any example to see how to add global parameters across all subcommands?

@dbanty
Copy link
Author

dbanty commented May 18, 2020

@kesavkolla yes you just add a callback function for your app and then use the --version parameter in that callback.

Docs on callback: https://typer.tiangolo.com/tutorial/commands/callback/

Docs on --version: https://typer.tiangolo.com/tutorial/options/version/

Example combining the two:

import typer

app = typer.Typer()

def version_callback(value: bool):
    if value:
        typer.echo(f"Awesome CLI Version: {__version__}")
        raise typer.Exit()

@app.callback()
def main(
    version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True),
):
    # Do other global stuff, handle other global options here
    return

@ssbarnea
Copy link
Contributor

@dbanty The example above is incomplete and is missing a critical part to at the end:

if __name__ == "__main__":
    app()

@dbanty
Copy link
Author

dbanty commented Mar 30, 2021

@dbanty The example above is incomplete and is missing a critical part to at the end:

If you’re running the script directly, yeah you’ll need that. I always include my entrypoints in my Poetry scripts so they’re installable. I often forget how non-poetry users do things 😅.

@ssbarnea
Copy link
Contributor

ssbarnea commented Mar 30, 2021

In fact I needed a bit longer version to make it work when called as a module because you need another method to be called as entry point, you cannot use the main() from the example. Full solutions added this to the bottom:

def cli():
    app()

if __name__ == "__main__":
    cli()

With that inside setup.cfg:

[options.entry_points]
console_scripts =
    foo=foo.__main__:cli

What I show above works with all 3 caling methods:

  • python -m foo
  • foo # after installing the package
  • python src/foo/__main__.py

There is one small bit that is not sorted yet and related to flake8 producing b008 do not perform function calls in argument defaults for the typer.Option() call. While I know how to add a noqa for it, I wonder if there a better alternative.

@povilasb
Copy link

@ssbarnea the same workaround as suggested for FastAPI works: fastapi/fastapi#1522 (comment)

@shelper
Copy link

shelper commented May 27, 2022

would it be even greater if the version value is read from poetry's pyproject.toml file?

maybe something like a typer plugin for poetry, so it generates this version CLI option automatically?

@pythoninthegrass
Copy link

Not to necro the thread, but it took me quite a few tries to get version flags working. Was able to modify @dbanty's post to look like this:

import typer
from myapp import __version__

app = typer.Typer()

def version_callback(value: bool):
    if value:
        print(__version__)
        raise typer.Exit()

@app.callback()
def version(
    version: bool = typer.Option(None,
                                 "--version", "-v",
                                 callback=version_callback,
                                 is_eager=True,
                                 help="Print the version and exit")
):
    pass

if __name__ == "__main__":
    app()

@shelper FWIW I'm using poetry as well and when I build my tiny app, it's possible to bump the __version__ semi-automatically in __init__.py. I really only do that when testing a prerelease.

For truly automatic semver, release-please is sweet.

@harkabeeparolus
Copy link

harkabeeparolus commented Apr 18, 2024

I agree that click.version_option() is pretty awesome, since it autodetects the package name, and then gets the program version from importlib.metadata.

I think the original suggestion of automatically adding "--version" to a Typer app would be awesome, e.g.:

# auto detect package name and version
app = typer.Typer(version_option=True)

# specify version string manually
app = typer.Typer(version_option="myapp v1.3.5")

@maxime1907
Copy link

maxime1907 commented Jul 5, 2024

If you want it automatically, you can do it that way:

import inspect

import typer

cli = typer.Typer(help="CLI for mypackage", pretty_exceptions_enable=False)

def get_package_name() -> str:
    frame = inspect.currentframe()
    f_back = frame.f_back if frame is not None else None
    f_globals = f_back.f_globals if f_back is not None else None
    # break reference cycle
    # https://docs.python.org/3/library/inspect.html#the-interpreter-stack
    del frame

    package_name: str | None = None
    if f_globals is not None:
        package_name = f_globals.get("__name__")

        if package_name == "__main__":
            package_name = f_globals.get("__package__")

        if package_name:
            package_name = package_name.partition(".")[0]
    if package_name is None:
        raise RuntimeError("Could not determine the package name automatically.")
    return package_name


def get_version(*, package_name: str) -> str:
    import importlib.metadata

    version: str | None = None
    try:
        version = importlib.metadata.version(package_name)
    except importlib.metadata.PackageNotFoundError:
        raise RuntimeError(f"{package_name!r} is not installed.") from None

    if version is None:
        raise RuntimeError(f"Could not determine the version for {package_name!r} automatically.")

    return version


def version_callback(*, value: bool):
    if value:
        package_name = get_package_name()
        version = get_version(package_name=package_name)
        typer.echo(f"{package_name}, {version}")
        raise typer.Exit()


@cli.callback()
def callback(
    version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True),
) -> None:
    pass


if __name__ == "__main__":
    cli()

@harkabeeparolus
Copy link

harkabeeparolus commented Jul 9, 2024

If you want it automatically, you can do it that way:

[...]

The point is, a "--version" option is something basically every CLI tool should have. This strongly suggests it should be provided by the framework, similary to the "--help" option. Click adds "--version" with one line of code, and you do not need to understand any advanced concepts, or have extensive familiarity with the abstractions of your framework. Just tab complete it in your editor, and it's done.

Typer should provide a nice default mechanism for this, at least as good and as easy as Click, rather than forcing everyone to implement it themselves using Typer callbacks... Which, frankly, is a construct that I didn't really understand the first few times I read through the documentation.

Some lines from The Zen of Python:

  • Beautiful is better than ugly.
  • Simple is better than complex.
  • There should be one-- and preferably only one --obvious way to do it.
  • If the implementation is hard to explain, it's a bad idea.

@alanwilter
Copy link

Is https://typer.tiangolo.com/tutorial/options/version/ still the way to go?
It really cannot get simpler like the "help" method or what @harkabeeparolus suggested?

@mattmess1221
Copy link

I don't like the idea of having an unused parameter version. Luckily, I can still manipulate the command/group instance by changing the cls.

import click
from typer import Typer
from typer.core import TyperCommand  # or TyperGroup


class MyCommand(TyperCommand):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        click.version_option(prog_name="testapp")(self)


app = Typer()  # or cls=MyGroup


@app.command(cls=MyCommand)
def main():
    print("Hello!")


if __name__ == "__main__":
    app()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature, enhancement or request
Projects
None yet
Development

No branches or pull requests