Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions tests/assets/cli/func_other_name_with_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from pathlib import Path

app = "not a Typer app"


def some_function(name: str = "World"):
print(f"Hello {name} from {Path('.').name or 'here'}")
11 changes: 11 additions & 0 deletions tests/assets/cli/multi_app_any_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typer

zeta = typer.Typer()


@zeta.command()
def hello():
print("zeta")


alpha = typer.Typer()
33 changes: 33 additions & 0 deletions tests/test_cli/test_app_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
import subprocess
import sys


def run_with_seed(asset: str, seed: int, *args: str) -> str:
env = {**os.environ, "PYTHONHASHSEED": str(seed)}
result = subprocess.run(
[sys.executable, "-m", "coverage", "run", "-m", "typer", asset, "run", *args],
capture_output=True,
encoding="utf-8",
env=env,
)
return result.stdout


def test_first_app_in_file_is_used_for_any_seed():
outputs = {
run_with_seed("tests/assets/cli/multi_app_any_name.py", seed)
for seed in range(8)
}
assert outputs == {"zeta\n"}


def test_first_function_in_file_is_used_not_an_import():
outputs = {
run_with_seed(
"tests/assets/cli/func_other_name_with_import.py", seed, "--name", "Camila"
)
for seed in range(8)
}
assert len(outputs) == 1
assert "Hello Camila" in outputs.pop()
32 changes: 18 additions & 14 deletions typer/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import importlib.util
import inspect
import re
import sys
from pathlib import Path
Expand Down Expand Up @@ -86,18 +87,19 @@ def get_typer_from_module(module: Any) -> typer.Typer | None:
sub_app = typer.Typer()
sub_app.command()(func_obj)
return sub_app
# Iterate and get a default object to use as CLI
local_names = dir(module)
local_names_set = set(local_names)
# Iterate and get a default object to use as CLI, in the order the names
# were defined in the file
local_names = [name for name in vars(module) if not name.startswith("__")]
# Try to get a default Typer app
for name in default_app_names:
if name in local_names_set:
obj = getattr(module, name, None)
if isinstance(obj, typer.Typer):
return obj
# Try to get any Typer app
for name in local_names_set - set(default_app_names):
obj = getattr(module, name)
obj = getattr(module, name, None)
if isinstance(obj, typer.Typer):
return obj
# Try to get the first Typer app
for name in local_names:
if name in default_app_names:
continue
obj = getattr(module, name, None)
if isinstance(obj, typer.Typer):
return obj
# Try to get a default function
Expand All @@ -107,10 +109,12 @@ def get_typer_from_module(module: Any) -> typer.Typer | None:
sub_app = typer.Typer()
sub_app.command()(func_obj)
return sub_app
# Try to get any func app
for func_name in local_names_set - set(default_func_names):
func_obj = getattr(module, func_name)
if callable(func_obj):
# Try to get the first function defined in the file
for func_name in local_names:
if func_name in default_func_names:
continue
func_obj = getattr(module, func_name, None)
if inspect.isfunction(func_obj) and func_obj.__module__ == module.__name__:
sub_app = typer.Typer()
sub_app.command()(func_obj)
return sub_app
Expand Down
Loading