Skip to content

Instantly share code, notes, and snippets.

@rino0601
Created Jul 7, 2022
Embed
What would you like to do?
Typer 에서 FastAPI 의 Depends 비슷한거 해보는 방법
from typing import Any, Optional, Union
import click
import pydantic
import typer
VERSION = "0.0.0"
# PATCH === https://github.com/tiangolo/typer/issues/111 === BEGIN
_get_click_type = typer.main.get_click_type
def supersede_get_click_type(
*, annotation: Any, parameter_info: typer.main.ParameterInfo
) -> click.ParamType:
if not hasattr(annotation, "parse_raw"):
return _get_click_type(annotation=annotation, parameter_info=parameter_info)
class CustomParamType(click.ParamType):
name = annotation.__name__
def convert(self, value, param, ctx):
if isinstance(value, annotation):
return value
return annotation.parse_raw(value)
return CustomParamType()
typer.main.get_click_type = supersede_get_click_type
# PATCH === https://github.com/tiangolo/typer/issues/111 === END
class User(pydantic.BaseModel):
id: int
name: str = "Jane Doe"
@staticmethod
def from_context(ctx: typer.Context) -> 'User':
if ctx is None: # 이게 최상위다. 못 찾은 셈.
raise typer.BadParameter("암튼 잘못 입력함")
for param in ctx.params.values(): # 모든 params 를 다 뒤져서
if isinstance(param, User): # 내가 원하던 타입이면 반환
return param
# 못 찾았으면 부모쪽에서 한번 더 찾아보기
return User.from_context(ctx.parent)
@staticmethod
def typer_callback(ctx: typer.Context, value: Union[str, 'User']) -> Optional['User']:
if ctx.resilient_parsing:
# 이게 필요한 이유는 아래 가이드를 참고하세요.
# https://typer.tiangolo.com/tutorial/options/callback-and-context/#handling-completion
return None
if isinstance(value, User):
# 사용자가 명시적으로 넘긴 경우
# e.g) --user '{"id": "2"}'
return value
try:
# parent? command 쪽에 혹시 있는지 확인. (옵션 이름이 같아야 함.)
return User.from_context(ctx)
except typer.BadParameter:
# 새로 초기화. 할 필요가 있는 경우.
# 먼저 선언된 params 를 가져 올 수 있음.
_id = ctx.params['id']
# 모자란 부분을 사용자에게 물어 볼 수 있음.
person_name = typer.prompt("What's your name?")
# 형 변환은 pydantic 이 적절히 해줄 것임
return User.parse_obj(dict(id=_id, name=person_name))
@staticmethod
def option(default):
return typer.Option(default, callback=User.typer_callback)
app = typer.Typer()
def version_callback(value: bool):
if value:
typer.echo(f"{VERSION}")
raise typer.Exit()
@app.callback()
def init(
version: Optional[bool] = typer.Option(None, "--version", callback=version_callback)
):
_ = version # pylint 를 속이기 위함
@app.command(help="이해를 돕기 위한 사례 1")
def foo(
ctx: typer.Context,
id: int = typer.Option(None),
user: User = typer.Option(None, callback=User.typer_callback), # ... 을 default 로 넘기면 Option 이지만 Required 하게 할 수 있음
):
_ = ctx, id # pylint 를 속이기 위함
typer.echo(f"{user}")
sub = typer.Typer()
@sub.callback()
def bar(
user: User = User.option(User(id=999, name="provided_default_user"))
):
"""어거지 예시지만, parent 에서 뽑아온다는걸 보여주기 위한"""
_ = user
@sub.command()
def foo(
user: User = User.option(None),
):
typer.echo(f"{user}")
if __name__ == '__main__':
app.add_typer(sub)
app()
@rino0601
Copy link
Author

rino0601 commented Jul 7, 2022

python cli.py foo --id 3
python cli.py foo --user '{"id": "2", "name":"wow"}'
python cli.py bar foo
python cli.py bar foo --user '{"id": "3", "name":"wow222"}'
python cli.py bar --user '{"id": "4", "name":"222wow222"}' foo
python cli.py bar --user '{"id": "4", "name":"222wow222"}' foo --user '{"id": "7", "name":"hhhh"}'

각각의 차이를 관찰해보세요

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment