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

Need feature to share options or arguments between commands #405

Open
7 tasks done
allinhtml opened this issue Jun 15, 2022 · 14 comments
Open
7 tasks done

Need feature to share options or arguments between commands #405

allinhtml opened this issue Jun 15, 2022 · 14 comments
Labels
question Question or problem

Comments

@allinhtml
Copy link

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the Typer documentation, with the integrated search.
  • I already searched in Google "How to X in Typer" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to Typer but to Click.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from typing import Optional, List

import typer
import os

app = typer.Typer()

@app.command()
def start(
  debug: bool = typer.Option(False),
  output_dir: str = typer.Option(os.getcwd()),
  flows: Optional[List[str]] = typer.Option(None, "--flow", "-f")):
  typer.echo(f"Debug mode: {debug}")
  typer.echo(f"Output Dir: {output_dir}")
  typer.echo(f"start flows: {flows}")


@app.command()
def stop(
  debug: bool = typer.Option(False),
  output_dir: str = typer.Option(os.getcwd())):
  typer.echo(f"Debug mode: {debug}")
  typer.echo(f"Output Dir: {output_dir}")
  typer.echo("STOP!")

@app.command()
def clean(
  debug: bool = typer.Option(False),
  output_dir: str = typer.Option(os.getcwd())):
  typer.echo(f"Debug mode: {debug}")
  typer.echo(f"Output Dir: {output_dir}")
  typer.echo("STOP!")


if __name__ == "__main__":
    app()

Description

How can we easily add common options into multiple commands like debug or output_directory?

Related question - #153 - But this is not working as expected. Help section don't show message properly as commented here.

Operating System

Linux, Windows, macOS

Operating System Details

No response

Typer Version

ALL

Python Version

ALL

Additional Context

No response

@allinhtml allinhtml added the question Question or problem label Jun 15, 2022
@Andrew-Sheridan
Copy link

Andrew-Sheridan commented Jul 3, 2022

@allinhtml One way to accomplish this is to have a module level object, a dataclass instance or a dict etc, to hold the state, and a callback to set it.

from typing import Optional, List

import typer
import os

app = typer.Typer(add_completion=False)
state = {}


@app.callback()
def _main(
    debug: bool = typer.Option(False, "--debug", help="If set print debug messages"),
    output_dir: str = typer.Option(os.getcwd(), help="The output directory"),
):
    state["debug"] = debug
    state["output_dir"] = output_dir


@app.command()
def start(
    flows: Optional[List[str]] = typer.Option(None, "--flow", "-f"),
):
    typer.echo(f"Debug mode: {state['debug']}")
    typer.echo(f"Output Dir: {state['output_dir']}")
    typer.echo(f"start flows: {flows}")


@app.command()
def stop():
    typer.echo(f"Debug mode: {state['debug']}")
    typer.echo(f"Output Dir: {state['output_dir']}")
    typer.echo("STOP!")


@app.command()
def clean():
    typer.echo(f"Debug mode: {state['debug']}")
    typer.echo(f"Output Dir: {state['output_dir']}")
    typer.echo("STOP!")


if __name__ == "__main__":
    app()

with debug:

python issue.py --debug --output-dir GitHub start
Debug mode: True
Output Dir: GitHub
start flows: ()

without:

python issue.py --output-dir GitHub clean
Debug mode: False
Output Dir: GitHub
STOP!

:)

@Zaubeerer
Copy link

Zaubeerer commented Jul 21, 2022

Is there a way to create common CLI options such, that they are configurable on the sub command level instead of on the command level?

e.g.

Inherited/Shared Options
- common option 1 ...
- common option 2 ...

Options
- subcommand specific option 1 ...
- subcommand specific option 2 ...

So that the command could be given as follows:

python issue.py clean --output-dir GitHub

@jimkring
Copy link

I need this feature, too. I posted some code related to working around this, here. The workarounds are "OK" but not great.

@chrisjsewell
Copy link

You can use the Context object no?

import typer
app = typer.Typer()

@app.callback()
def main_app(
	ctx: typer.Context,
    verbose: Optional[bool] = typer.Option(None, help="Enable verbose mode.")
):
	obj = ctx.ensure_object(dict)
    obj["verbose"] = verbose

@app.command()
def test(ctx: typer.Context):
    obj = ctx.ensure_object(dict)
    print(obj)

see https://typer.tiangolo.com/tutorial/commands/context/
and https://click.palletsprojects.com/en/8.1.x/complex/

@Zaubeerer
Copy link

Will have to check that out, hopefully this weekend.

@rodonn
Copy link

rodonn commented May 19, 2023

Not a perfect solution, but what I've been doing is defining the Option and Argument options outside of the function arguments and then reusing them for functions that share the same arguments.

from typing import Optional, List, Annotated

import typer
import os

app = typer.Typer()


debug_option = typer.Option(
    "--debug",
    "-d",
    help="Enable debug mode.",
    show_default=True,
    default_factory=lambda: True,
)

output_dir_option = typer.Option(
    "--output-dir",
    "-o",
    help="Output directory for the generated files.",
    show_default=True,
    default_factory=os.getcwd,
)

flows_option = typer.Option(
    "--flow",
    "-f",
    help="Start flows.",
    show_default=True,
    default_factory=lambda: None,
)


@app.command()
def start(
    flows: Annotated[Optional[List[str]], flows_option],
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo(f"start flows: {flows}")


@app.command()
def stop(
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo("STOP!")

@app.command()
def clean(
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo("STOP!")


if __name__ == "__main__":
        app()

@NikosAlexandris
Copy link

Not a perfect solution, but what I've been doing is defining the Option and Argument options outside of the function arguments and then reusing them for functions that share the same arguments.

from typing import Optional, List, Annotated

import typer
import os

app = typer.Typer()


debug_option = typer.Option(
    "--debug",
    "-d",
    help="Enable debug mode.",
    show_default=True,
    default_factory=lambda: True,
)

output_dir_option = typer.Option(
    "--output-dir",
    "-o",
    help="Output directory for the generated files.",
    show_default=True,
    default_factory=os.getcwd,
)

flows_option = typer.Option(
    "--flow",
    "-f",
    help="Start flows.",
    show_default=True,
    default_factory=lambda: None,
)


@app.command()
def start(
    flows: Annotated[Optional[List[str]], flows_option],
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo(f"start flows: {flows}")


@app.command()
def stop(
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo("STOP!")

@app.command()
def clean(
    output_dir: Annotated[str, output_dir_option],
    debug: Annotated[bool, debug_option],
):
    typer.echo(f"Debug mode: {debug}")
    typer.echo(f"Output Dir: {output_dir}")
    typer.echo("STOP!")


if __name__ == "__main__":
        app()

Actually, this is a good solution. Listing the input arguments in a function's definition (signature) makes code that is easy to read and understand. Defining typer Options and Arguments only once, is less error prone and economical. Thank you.

@robinbowes
Copy link

robinbowes commented Aug 9, 2023

I'm using something similar to the previous example to share options.

Here's a contrived example:

from datetime import datetime
from types import SimpleNamespace
from typing import Annotated, Optional

import typer

app = typer.Typer()

options = SimpleNamespace(
    start_date=Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help="Start date",
        ),
    ],
    end_date=Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help="End date",
        ),
    ],
)


@app.command()
def main(
    start_date: options.start_date = None,
    end_date: options.end_date = None,
):
    print(f"{start_date} - {end_date}")


if __name__ == "__main__":
    app()

Note that the only difference between the start_date and end_date options is the help text.

I'm trying to figure out some way I can use a single date option, and set the help text in the command, ie. something like this:

@app.command()
def main(
    start_date: options.date(help="Start date") = None,
    end_date: options.date(help="End date") = None,
):
    print(f"{start_date} - {end_date}")

Anyone got any ideas how I might implement this?

@robinbowes
Copy link

I tried this:

def make_date_option(help="Enter a date"):
    return Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help=help,
        ),
    ],

@app.command()
def main(
    start_date: make_date_option(help="Start date") = None,
    end_date: make_date_option(help="End date") = None,
):
    print(f"{start_date} - {end_date}")

Sadly, this throws a couple of flake8 syntax errors:

  • error| syntax error in forward annotation 'Start date' [F722]
  • error| syntax error in forward annotation 'End date' [F722]

@NikosAlexandris
Copy link

def make_date_option(help="Enter a date"):
return Annotated[
Optional[datetime],
typer.Option(
formats=["%Y-%m-%d"],
help=help,
),
],

@app.command()
def main(
start_date: make_date_option(help="Start date") = None,
end_date: make_date_option(help="End date") = None,
):
print(f"{start_date} - {end_date}")

Following works for me :

❯ cat test.py
from datetime import datetime
from types import SimpleNamespace
from typing import Annotated, Optional
import typer

app = typer.Typer()

def make_date_option(help="Enter a date"):
    return Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help=help,
        ),
    ]

@app.command()
def main(
    start_date: make_date_option(help='Start') = None,
    end_date: make_date_option(help='End') = None,
):
    print(f"{start_date} - {end_date}")

and

❯ typer test.py run --start-date '2010-01-01' --end-date '2011-02-02'
2010-01-01 00:00:00 - 2011-02-02 00:00:00

I think you have a comma left-over in the end of the make_date_option().

@robinbowes
Copy link

robinbowes commented Oct 19, 2023

I think you have a comma left-over in the end of the make_date_option().

Good spot, but I think that's a copy/paste error and not the source of the F722 error.

Re-visiting, I've found that this code runs just fine (with the extra comma removed), but the Syntastic flake8 check still throws the error.

Using the power of google, I found this solution: https://stackoverflow.com/a/73235243

Adding SimpleNamesapce to the mix, I ended up with this final code:

from datetime import datetime
from types import SimpleNamespace
from typing import Annotated, Optional

import typer

app = typer.Typer()


def make_date_option(help="Enter a date"):
    return Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help=help,
        ),
    ]


options = SimpleNamespace(
    start_date=make_date_option(help="Start date"),
    end_date=make_date_option(help="End date"),
)


@app.command()
def main(
    start_date: options.start_date = None,
    end_date: options.end_date = None,
):
    print(f"{start_date} - {end_date}")


if __name__ == "__main__":
    app()

In a real project, make_date_option and the options object would be defined in a separate module and imported wherever required.

@NikosAlexandris
Copy link

NikosAlexandris commented Nov 3, 2023

I think you have a comma left-over in the end of the make_date_option().

Good spot, but I think that's a copy/paste error and not the source of the F722 error.

Re-visiting, I've found that this code runs just fine (with the extra comma removed), but the Syntastic flake8 check still throws the error.

Using the power of google, I found this solution: https://stackoverflow.com/a/73235243

Adding SimpleNamesapce to the mix, I ended up with this final code:

from datetime import datetime
from types import SimpleNamespace
from typing import Annotated, Optional

import typer

app = typer.Typer()


def make_date_option(help="Enter a date"):
    return Annotated[
        Optional[datetime],
        typer.Option(
            formats=["%Y-%m-%d"],
            help=help,
        ),
    ]


options = SimpleNamespace(
    start_date=make_date_option(help="Start date"),
    end_date=make_date_option(help="End date"),
)


@app.command()
def main(
    start_date: options.start_date = None,
    end_date: options.end_date = None,
):
    print(f"{start_date} - {end_date}")


if __name__ == "__main__":
    app()

In a real project, make_date_option and the options object would be defined in a separate module and imported wherever required.

I have many shared options, as per your options.start_date and .end_date. Do you think it's worth the effort to organise them thematically using the SimpleNamespace() like you do? This means practically that I can avoid importing multiple options defined elsewhere and one import would suffice. Right?

@robinbowes
Copy link

I have many shared options, as per your options.start_date and .end_date. Do you think it's worth the effort to organise them thematically using the SimpleNamespace() like you do? This means practically that I can avoid importing multiple options defined elsewhere and one import would suffice. Right?

Only you can decide what's "worth the effort"

@palto42
Copy link

palto42 commented Jan 6, 2024

debug_option = typer.Option(
"--debug",
"-d",
help="Enable debug mode.",
show_default=True,
default_factory=lambda: True,
)

I don't understand why this only forks with default_factory=lambda: True, which looks a bit odd.

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

No branches or pull requests

9 participants