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

Common base class for all non-exit exceptions.

AnnotatedParamWithDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
28    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
29        self.argument_name = argument_name
30        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):
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

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])
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
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):
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        )

Common base class for all non-exit exceptions.

MultipleTyperAnnotationsError(argument_name: str)
70    def __init__(self, argument_name: str):
71        self.argument_name = argument_name
argument_name: str
Inherited Members
builtins.BaseException
with_traceback
add_note
args
class DefaultFactoryAndDefaultValueError(builtins.Exception):
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        )

Common base class for all non-exit exceptions.

DefaultFactoryAndDefaultValueError(argument_name: str, param_type: Type[typer.models.ParameterInfo])
84    def __init__(self, argument_name: str, param_type: Type[ParameterInfo]):
85        self.argument_name = argument_name
86        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]:
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