typer.utils

  1import inspect
  2import sys
  3from copy import copy
  4from typing import Any, Callable, Dict, List, Tuple, Type, cast
  5
  6from typing_extensions import Annotated, get_type_hints
  7
  8from ._typing import get_args, get_origin
  9from .models import ArgumentInfo, OptionInfo, ParameterInfo, ParamMeta
 10
 11
 12def _param_type_to_user_string(param_type: Type[ParameterInfo]) -> str:
 13    # Render a `ParameterInfo` subclass for use in error messages.
 14    # User code doesn't call `*Info` directly, so errors should present the classes how
 15    # they were (probably) defined in the user code.
 16    if param_type is OptionInfo:
 17        return "`Option`"
 18    elif param_type is ArgumentInfo:
 19        return "`Argument`"
 20    # This line shouldn't be reachable during normal use.
 21    return f"`{param_type.__name__}`"  # pragma: no cover
 22
 23
 24class AnnotatedParamWithDefaultValueError(Exception):
 25    argument_name: str
 26    param_type: Type[ParameterInfo]
 27
 28    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
 29        self.argument_name = argument_name
 30        self.param_type = param_type
 31
 32    def __str__(self) -> str:
 33        param_type_str = _param_type_to_user_string(self.param_type)
 34        return (
 35            f"{param_type_str} default value cannot be set in `Annotated`"
 36            f" for {self.argument_name!r}. Set the default value with `=` instead."
 37        )
 38
 39
 40class MixedAnnotatedAndDefaultStyleError(Exception):
 41    argument_name: str
 42    annotated_param_type: Type[ParameterInfo]
 43    default_param_type: Type[ParameterInfo]
 44
 45    def __init__(
 46        self,
 47        argument_name: str,
 48        annotated_param_type: Type[ParameterInfo],
 49        default_param_type: Type[ParameterInfo],
 50    ):
 51        self.argument_name = argument_name
 52        self.annotated_param_type = annotated_param_type
 53        self.default_param_type = default_param_type
 54
 55    def __str__(self) -> str:
 56        annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type)
 57        default_param_type_str = _param_type_to_user_string(self.default_param_type)
 58        msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and"
 59        if self.annotated_param_type is self.default_param_type:
 60            msg += " default value"
 61        else:
 62            msg += f" {default_param_type_str} as a default value"
 63        msg += f" together for {self.argument_name!r}"
 64        return msg
 65
 66
 67class MultipleTyperAnnotationsError(Exception):
 68    argument_name: str
 69
 70    def __init__(self, argument_name: str):
 71        self.argument_name = argument_name
 72
 73    def __str__(self) -> str:
 74        return (
 75            "Cannot specify multiple `Annotated` Typer arguments"
 76            f" for {self.argument_name!r}"
 77        )
 78
 79
 80class DefaultFactoryAndDefaultValueError(Exception):
 81    argument_name: str
 82    param_type: Type[ParameterInfo]
 83
 84    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
 85        self.argument_name = argument_name
 86        self.param_type = param_type
 87
 88    def __str__(self) -> str:
 89        param_type_str = _param_type_to_user_string(self.param_type)
 90        return (
 91            "Cannot specify `default_factory` and a default value together"
 92            f" for {param_type_str}"
 93        )
 94
 95
 96def _split_annotation_from_typer_annotations(
 97    base_annotation: Type[Any],
 98) -> Tuple[Type[Any], List[ParameterInfo]]:
 99    if get_origin(base_annotation) is not Annotated:  # type: ignore
100        return base_annotation, []
101    base_annotation, *maybe_typer_annotations = get_args(base_annotation)
102    return base_annotation, [
103        annotation
104        for annotation in maybe_typer_annotations
105        if isinstance(annotation, ParameterInfo)
106    ]
107
108
109def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
110    if sys.version_info >= (3, 10):
111        signature = inspect.signature(func, eval_str=True)
112    else:
113        signature = inspect.signature(func)
114
115    type_hints = get_type_hints(func)
116    params = {}
117    for param in signature.parameters.values():
118        annotation, typer_annotations = _split_annotation_from_typer_annotations(
119            param.annotation,
120        )
121        if len(typer_annotations) > 1:
122            raise MultipleTyperAnnotationsError(param.name)
123
124        default = param.default
125        if typer_annotations:
126            # It's something like `my_param: Annotated[str, Argument()]`
127            [parameter_info] = typer_annotations
128
129            # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
130            if isinstance(param.default, ParameterInfo):
131                raise MixedAnnotatedAndDefaultStyleError(
132                    argument_name=param.name,
133                    annotated_param_type=type(parameter_info),
134                    default_param_type=type(param.default),
135                )
136
137            parameter_info = copy(parameter_info)
138
139            # When used as a default, `Option` takes a default value and option names
140            # as positional arguments:
141            #   `Option(some_value, "--some-argument", "-s")`
142            # When used in `Annotated` (ie, what this is handling), `Option` just takes
143            # option names as positional arguments:
144            #   `Option("--some-argument", "-s")`
145            # In this case, the `default` attribute of `parameter_info` is actually
146            # meant to be the first item of `param_decls`.
147            if (
148                isinstance(parameter_info, OptionInfo)
149                and parameter_info.default is not ...
150            ):
151                parameter_info.param_decls = (
152                    cast(str, parameter_info.default),
153                    *(parameter_info.param_decls or ()),
154                )
155                parameter_info.default = ...
156
157            # Forbid `my_param: Annotated[str, Argument('some-default')]`
158            if parameter_info.default is not ...:
159                raise AnnotatedParamWithDefaultValueError(
160                    param_type=type(parameter_info),
161                    argument_name=param.name,
162                )
163            if param.default is not param.empty:
164                # Put the parameter's default (set by `=`) into `parameter_info`, where
165                # typer can find it.
166                parameter_info.default = param.default
167
168            default = parameter_info
169        elif param.name in type_hints:
170            # Resolve forward references.
171            annotation = type_hints[param.name]
172
173        if isinstance(default, ParameterInfo):
174            parameter_info = copy(default)
175            # Click supports `default` as either
176            # - an actual value; or
177            # - a factory function (returning a default value.)
178            # The two are not interchangeable for static typing, so typer allows
179            # specifying `default_factory`. Move the `default_factory` into `default`
180            # so click can find it.
181            if parameter_info.default is ... and parameter_info.default_factory:
182                parameter_info.default = parameter_info.default_factory
183            elif parameter_info.default_factory:
184                raise DefaultFactoryAndDefaultValueError(
185                    argument_name=param.name, param_type=type(parameter_info)
186                )
187            default = parameter_info
188
189        params[param.name] = ParamMeta(
190            name=param.name, default=default, annotation=annotation
191        )
192    return params
class AnnotatedParamWithDefaultValueError(builtins.Exception):
25class AnnotatedParamWithDefaultValueError(Exception):
26    argument_name: str
27    param_type: Type[ParameterInfo]
28
29    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
30        self.argument_name = argument_name
31        self.param_type = param_type
32
33    def __str__(self) -> str:
34        param_type_str = _param_type_to_user_string(self.param_type)
35        return (
36            f"{param_type_str} default value cannot be set in `Annotated`"
37            f" for {self.argument_name!r}. Set the default value with `=` instead."
38        )

Common base class for all non-exit exceptions.

AnnotatedParamWithDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
29    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
30        self.argument_name = argument_name
31        self.param_type = param_type
argument_name: str
param_type: Type[typer.models.ParameterInfo]
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class MixedAnnotatedAndDefaultStyleError(builtins.Exception):
41class MixedAnnotatedAndDefaultStyleError(Exception):
42    argument_name: str
43    annotated_param_type: Type[ParameterInfo]
44    default_param_type: Type[ParameterInfo]
45
46    def __init__(
47        self,
48        argument_name: str,
49        annotated_param_type: Type[ParameterInfo],
50        default_param_type: Type[ParameterInfo],
51    ):
52        self.argument_name = argument_name
53        self.annotated_param_type = annotated_param_type
54        self.default_param_type = default_param_type
55
56    def __str__(self) -> str:
57        annotated_param_type_str = _param_type_to_user_string(self.annotated_param_type)
58        default_param_type_str = _param_type_to_user_string(self.default_param_type)
59        msg = f"Cannot specify {annotated_param_type_str} in `Annotated` and"
60        if self.annotated_param_type is self.default_param_type:
61            msg += " default value"
62        else:
63            msg += f" {default_param_type_str} as a default value"
64        msg += f" together for {self.argument_name!r}"
65        return msg

Common base class for all non-exit exceptions.

MixedAnnotatedAndDefaultStyleError( argument_name: str, annotated_param_type: Type[typer.models.ParameterInfo], default_param_type: Type[typer.models.ParameterInfo])
46    def __init__(
47        self,
48        argument_name: str,
49        annotated_param_type: Type[ParameterInfo],
50        default_param_type: Type[ParameterInfo],
51    ):
52        self.argument_name = argument_name
53        self.annotated_param_type = annotated_param_type
54        self.default_param_type = default_param_type
argument_name: str
annotated_param_type: Type[typer.models.ParameterInfo]
default_param_type: Type[typer.models.ParameterInfo]
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class MultipleTyperAnnotationsError(builtins.Exception):
68class MultipleTyperAnnotationsError(Exception):
69    argument_name: str
70
71    def __init__(self, argument_name: str):
72        self.argument_name = argument_name
73
74    def __str__(self) -> str:
75        return (
76            "Cannot specify multiple `Annotated` Typer arguments"
77            f" for {self.argument_name!r}"
78        )

Common base class for all non-exit exceptions.

MultipleTyperAnnotationsError(argument_name: str)
71    def __init__(self, argument_name: str):
72        self.argument_name = argument_name
argument_name: str
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class DefaultFactoryAndDefaultValueError(builtins.Exception):
81class DefaultFactoryAndDefaultValueError(Exception):
82    argument_name: str
83    param_type: Type[ParameterInfo]
84
85    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
86        self.argument_name = argument_name
87        self.param_type = param_type
88
89    def __str__(self) -> str:
90        param_type_str = _param_type_to_user_string(self.param_type)
91        return (
92            "Cannot specify `default_factory` and a default value together"
93            f" for {param_type_str}"
94        )

Common base class for all non-exit exceptions.

DefaultFactoryAndDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
85    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
86        self.argument_name = argument_name
87        self.param_type = param_type
argument_name: str
param_type: Type[typer.models.ParameterInfo]
Inherited Members
builtins.BaseException
with_traceback
add_note
args
def get_params_from_function(func: Callable[..., Any]) -> Dict[str, typer.models.ParamMeta]:
110def get_params_from_function(func: Callable[..., Any]) -> Dict[str, ParamMeta]:
111    if sys.version_info >= (3, 10):
112        signature = inspect.signature(func, eval_str=True)
113    else:
114        signature = inspect.signature(func)
115
116    type_hints = get_type_hints(func)
117    params = {}
118    for param in signature.parameters.values():
119        annotation, typer_annotations = _split_annotation_from_typer_annotations(
120            param.annotation,
121        )
122        if len(typer_annotations) > 1:
123            raise MultipleTyperAnnotationsError(param.name)
124
125        default = param.default
126        if typer_annotations:
127            # It's something like `my_param: Annotated[str, Argument()]`
128            [parameter_info] = typer_annotations
129
130            # Forbid `my_param: Annotated[str, Argument()] = Argument("...")`
131            if isinstance(param.default, ParameterInfo):
132                raise MixedAnnotatedAndDefaultStyleError(
133                    argument_name=param.name,
134                    annotated_param_type=type(parameter_info),
135                    default_param_type=type(param.default),
136                )
137
138            parameter_info = copy(parameter_info)
139
140            # When used as a default, `Option` takes a default value and option names
141            # as positional arguments:
142            #   `Option(some_value, "--some-argument", "-s")`
143            # When used in `Annotated` (ie, what this is handling), `Option` just takes
144            # option names as positional arguments:
145            #   `Option("--some-argument", "-s")`
146            # In this case, the `default` attribute of `parameter_info` is actually
147            # meant to be the first item of `param_decls`.
148            if (
149                isinstance(parameter_info, OptionInfo)
150                and parameter_info.default is not ...
151            ):
152                parameter_info.param_decls = (
153                    cast(str, parameter_info.default),
154                    *(parameter_info.param_decls or ()),
155                )
156                parameter_info.default = ...
157
158            # Forbid `my_param: Annotated[str, Argument('some-default')]`
159            if parameter_info.default is not ...:
160                raise AnnotatedParamWithDefaultValueError(
161                    param_type=type(parameter_info),
162                    argument_name=param.name,
163                )
164            if param.default is not param.empty:
165                # Put the parameter's default (set by `=`) into `parameter_info`, where
166                # typer can find it.
167                parameter_info.default = param.default
168
169            default = parameter_info
170        elif param.name in type_hints:
171            # Resolve forward references.
172            annotation = type_hints[param.name]
173
174        if isinstance(default, ParameterInfo):
175            parameter_info = copy(default)
176            # Click supports `default` as either
177            # - an actual value; or
178            # - a factory function (returning a default value.)
179            # The two are not interchangeable for static typing, so typer allows
180            # specifying `default_factory`. Move the `default_factory` into `default`
181            # so click can find it.
182            if parameter_info.default is ... and parameter_info.default_factory:
183                parameter_info.default = parameter_info.default_factory
184            elif parameter_info.default_factory:
185                raise DefaultFactoryAndDefaultValueError(
186                    argument_name=param.name, param_type=type(parameter_info)
187                )
188            default = parameter_info
189
190        params[param.name] = ParamMeta(
191            name=param.name, default=default, annotation=annotation
192        )
193    return params