typer.utils

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

Common base class for all non-exit exceptions.

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

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

Common base class for all non-exit exceptions.

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

Common base class for all non-exit exceptions.

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