Replies: 2 comments 3 replies
-
|
Typer doesn't have per-arg injection like FastAPI's def build_services(settings=None): # framework-agnostic, reuse anywhere
settings = settings or Settings(...)
return {"sync_users": SyncUsersCommand(UserRepository(ApiClient(settings)))}
@app.callback()
def main(ctx: typer.Context):
ctx.obj = build_services()
@app.command()
def sync_users(ctx: typer.Context):
typer.echo(ctx.obj["sync_users"].run())ran it on typer 0.23.2: the CLI path works, and |
Beta Was this translation helpful? Give feedback.
-
|
Big +1 on per-arg injection being on the roadmap - that'd be lovely. For anyone landing here now, the @app.callback() + ctx.obj pattern holds up really well in practice, and the part that sold me on it is how cleanly it tests. The trick is to keep one framework-agnostic build_services() and let Typer be a thin adapter: # services.py - knows nothing about Typer
def build_services(settings: Settings) -> Services:
# construct repos/clients/use-cases once
return Services(...)
# cli.py
@app.callback()
def main(ctx: typer.Context):
ctx.obj = build_services(load_settings())
@app.command()
def register(ctx: typer.Context, email: str):
ctx.obj.register_user.execute(email) # command stays thinBecause build_services() is plain Python, the same graph runs in a worker or a script with no Typer involved - and tests don't need Typer's runtime at all: def test_register():
services = build_services(test_settings())
services.register_user.repo = FakeRepo() # or build with fakes
services.register_user.execute("ada@example.com")And when you do want to exercise the CLI end to end, CliRunner + a callback that swaps in a test ctx.obj keeps it isolated: runner = CliRunner()
app_under_test = build_app(services=build_services(test_settings()))
result = runner.invoke(app_under_test, ["register", "ada@example.com"])The mental rule that's served me: Typer owns argument parsing and I/O; the application owns the service graph. Keeps commands thin and the wiring reusable across CLI, workers, and tests without duplicating setup. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Example Code
Description
I’m looking for guidance on the recommended Typer pattern for wiring shared services into commands.
In small CLI apps, creating objects directly inside each command is clear. The question starts when several commands share the same infrastructure: settings, API clients, repositories, and application services.
The example above uses a small factory function:
build_sync_users_command()
That works, but as the CLI grows it can lead to repeated setup code across commands. I’m trying to understand what Typer users usually prefer:
The goal is to keep Typer command functions thin: parse CLI arguments, get the command object, run it. I also want the application services to stay independent from Typer so they can be reused from tests, workers, or other entrypoints.
Operating System
macOS
Operating System Details
No response
Project Version
Not version-specific
Python Version
Not version-specific
Additional Context
This is an architecture / project-structure question, not a bug report.
I’m not asking Typer to provide a dependency injection system. I’m trying to understand the clean boundary between Typer command functions and application-level service wiring in larger CLI applications.
Beta Was this translation helpful? Give feedback.
All reactions