Skip to content

Commit c314f80

Browse files
committed
lazy definition of runtimeparam to avoid up-front cost
1 parent 134a0f9 commit c314f80

7 files changed

Lines changed: 50 additions & 39 deletions

File tree

tests/test_coercion.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pathlib import Path
22

33
import typer
4+
from typer.core import TyperParameter
45
from typer.testing import CliRunner
56

67
runner = CliRunner()
@@ -147,8 +148,9 @@ def main(val=Widget(42)):
147148
seen["val"] = val
148149

149150
param = next(p for p in typer.main.get_command(app).params if p.name == "val")
150-
assert param.runtime_param is not None
151-
assert param.runtime_param.annotation is Widget
151+
assert isinstance(param, TyperParameter)
152+
assert param.get_runtime_param() is not None
153+
assert param.get_runtime_param().annotation is Widget
152154

153155
result = runner.invoke(app)
154156
assert result.exit_code == 0
@@ -177,8 +179,9 @@ def main(val: Widget = typer.Option("42", parser=parse_widget)):
177179
seen["val"] = val
178180

179181
param = next(p for p in typer.main.get_command(app).params if p.name == "val")
180-
assert param.runtime_param is not None
181-
assert param.runtime_param.annotation is Widget
182+
assert isinstance(param, TyperParameter)
183+
assert param.get_runtime_param() is not None
184+
assert param.get_runtime_param().annotation is Widget
182185

183186
result = runner.invoke(app)
184187
assert result.exit_code == 0

tests/test_type_conversion.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010
import typer
1111
from typer import _click, param_types
12+
from typer.core import TyperParameter
1213
from typer.testing import CliRunner
1314

1415
from tests.utils import needs_linux, needs_windows
@@ -445,12 +446,15 @@ def cmd(val=default):
445446
pass # pragma: no cover
446447

447448
param = next(p for p in typer.main.get_command(app).params if p.name == "val")
448-
assert param.runtime_param is not None
449+
assert isinstance(param, TyperParameter)
450+
assert param.get_runtime_param() is not None
449451
if get_origin(expected_annotation) is tuple:
450-
assert get_origin(param.runtime_param.annotation) is tuple
451-
assert get_args(param.runtime_param.annotation) == get_args(expected_annotation)
452+
assert get_origin(param.get_runtime_param().annotation) is tuple
453+
assert get_args(param.get_runtime_param().annotation) == get_args(
454+
expected_annotation
455+
)
452456
else:
453-
assert param.runtime_param.annotation is expected_annotation
457+
assert param.get_runtime_param().annotation is expected_annotation
454458

455459

456460
@pytest.mark.parametrize(

typer/_click/decorators.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def decorator(f: Command) -> Command:
4040

4141
def help_option(param_decls: list[str]) -> Callable[[Command], Command]:
4242
"""Help option which prints the help page and exits the program."""
43-
from ..coercion import bool_flag_runtime_param, bool_flag_type_descriptor
43+
from ..coercion import bool_flag_type_descriptor
4444

4545
def show_help(ctx: Context, param: Parameter, value: bool) -> None:
4646
"""Callback that print the help page on ``<stdout>`` and exits."""
@@ -58,6 +58,5 @@ def show_help(ctx: Context, param: Parameter, value: bool) -> None:
5858
help="Show this message and exit.",
5959
callback=show_help,
6060
required=False,
61-
runtime_param=bool_flag_runtime_param(),
6261
type_descriptor=bool_flag_type_descriptor(),
6362
)

typer/adapters.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,28 @@ def try_build_adapter(
5252
return None
5353

5454

55+
def validate_annotation_structure(annotation: Any) -> None:
56+
"""Raise on invalid nested CLI annotations (developer errors)."""
57+
origin = get_origin(annotation)
58+
if origin is list:
59+
args = get_args(annotation)
60+
if len(args) != 1:
61+
raise ValueError(f"Expected one list item type, got: {args!r}")
62+
validate_annotation_structure(args[0])
63+
elif origin is tuple:
64+
for item_type in get_args(annotation):
65+
validate_annotation_structure(item_type)
66+
67+
5568
def build_adapter(annotation: Any, parameter_info: ParameterInfo) -> TypeAdapter[Any]:
5669
"""Build a Pydantic TypeAdapter for a parameter annotation and metadata.
5770
Check for list/tuple and call this function recursively.
5871
Otherwise, delegate to build_leaf_adapter.
5972
"""
6073
origin = get_origin(annotation)
6174
if origin is list:
62-
args = get_args(annotation)
63-
if len(args) != 1:
64-
raise ValueError(f"Expected one list item type, got: {args!r}")
65-
list_type = args[0]
75+
validate_annotation_structure(annotation)
76+
list_type = get_args(annotation)[0]
6677
adapter = build_adapter(list_type, parameter_info)
6778

6879
def parse_list(value: Any, info: ValidationInfo) -> list[Any]:

typer/coercion.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
class TypeDescriptor:
3636
annotation: ParameterAnnotation
3737
parameter_info: ParameterInfo
38-
adapter: TypeAdapter[Any] | None
3938
file_annotation: Any | None
4039

4140
@property
@@ -119,15 +118,12 @@ def resolve_type_descriptor(
119118
annotation: ParameterAnnotation,
120119
parameter_info: ParameterInfo,
121120
) -> TypeDescriptor:
122-
"""Resolve Pydantic adapter for one parameter annotation."""
121+
"""Create type descriptor for one parameter annotation."""
122+
adapters.validate_annotation_structure(annotation)
123123
file_annotation = file_coercion_annotation(annotation)
124-
adapter = None
125-
if file_annotation is None:
126-
adapter = adapters.try_build_adapter(annotation, parameter_info)
127124
return TypeDescriptor(
128125
annotation=annotation,
129126
parameter_info=parameter_info,
130-
adapter=adapter,
131127
file_annotation=file_annotation,
132128
)
133129

@@ -224,8 +220,9 @@ def build_runtime_param(descriptor: TypeDescriptor) -> RuntimeParam:
224220
}
225221
if descriptor.file_annotation is not None:
226222
return FileRuntimeParam(**args, file_annotation=descriptor.file_annotation)
227-
if descriptor.adapter is not None:
228-
return AdapterRuntimeParam(**args, adapter=descriptor.adapter)
223+
adapter = adapters.try_build_adapter(**args)
224+
if adapter is not None:
225+
return AdapterRuntimeParam(**args, adapter=adapter)
229226
return PassThroughRuntimeParam(**args)
230227

231228

@@ -237,11 +234,6 @@ def bool_flag_type_descriptor() -> TypeDescriptor:
237234
)
238235

239236

240-
def bool_flag_runtime_param() -> RuntimeParam:
241-
"""Build runtime coercion for a standalone boolean flag option."""
242-
return build_runtime_param(bool_flag_type_descriptor())
243-
244-
245237
def prompt_value_proc(
246238
param_type: Any | None = None,
247239
default: Any | None = None,

typer/core.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from ._click.parser import _OptionParser
2020
from ._click.shell_completion import CompletionItem
2121
from ._typing import Literal
22-
from .coercion import RuntimeParam, TypeDescriptor
22+
from .coercion import RuntimeParam, TypeDescriptor, build_runtime_param
2323
from .display import describe_number_range
2424
from .param_types import choice_as_str, normalize_choice_value
2525
from .utils import parse_boolean_env_var
@@ -88,12 +88,19 @@ def compat_autocompletion(
8888
class TyperParameter(_click.core.Parameter):
8989
"""Typer parameter with runtime coercion."""
9090

91-
runtime_param: RuntimeParam
91+
_runtime_param: RuntimeParam | None
9292
type_descriptor: TypeDescriptor
9393
show_choices: bool
9494

95+
def get_runtime_param(self) -> RuntimeParam:
96+
"""lazy definition to avoid up-front costs"""
97+
if self._runtime_param is None:
98+
self._runtime_param = build_runtime_param(self.type_descriptor)
99+
assert self._runtime_param is not None
100+
return self._runtime_param
101+
95102
def process_value(self, ctx: _click.Context, value: Any) -> Any:
96-
value = self.runtime_param.coerce(value, param=self, ctx=ctx)
103+
value = self.get_runtime_param().coerce(value, param=self, ctx=ctx)
97104
if self.required and self.value_is_missing(value):
98105
raise _click.exceptions.MissingParameter(ctx=ctx, param=self)
99106
if self.callback is not None:
@@ -200,7 +207,7 @@ def display_type_rich(self, ctx: _click.Context) -> str | None:
200207
return self.display_type(ctx)
201208

202209
def bare_type(self) -> str:
203-
annotation = self.runtime_param.annotation
210+
annotation = self.type_descriptor.annotation
204211
return self._bare_type(annotation)
205212

206213
def _bare_type(self, annotation: type) -> str:
@@ -389,7 +396,6 @@ def __init__(
389396
*,
390397
# Parameter
391398
param_decls: list[str],
392-
runtime_param: RuntimeParam,
393399
type_descriptor: TypeDescriptor,
394400
required: bool = False,
395401
default: Any | None = None,
@@ -427,8 +433,8 @@ def __init__(
427433
self.min = min
428434
self.max = max
429435
self.rich_help_panel = rich_help_panel
430-
self.runtime_param = runtime_param
431436
self.type_descriptor = type_descriptor
437+
self._runtime_param = None
432438

433439
super().__init__(
434440
param_decls=param_decls,
@@ -570,7 +576,6 @@ def __init__(
570576
*,
571577
# Parameter
572578
param_decls: list[str],
573-
runtime_param: RuntimeParam,
574579
type_descriptor: TypeDescriptor,
575580
required: bool = False,
576581
default: Any | None = None,
@@ -613,8 +618,8 @@ def __init__(
613618

614619
self.min = min
615620
self.max = max
616-
self.runtime_param = runtime_param
617621
self.type_descriptor = type_descriptor
622+
self._runtime_param = None
618623

619624
super().__init__(
620625
param_decls,

typer/main.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from . import _click
1717
from ._click.globals import get_current_context
1818
from ._typing import get_args, get_origin
19-
from .coercion import build_runtime_param, resolve_type_descriptor
19+
from .coercion import resolve_type_descriptor
2020
from .completion import get_completion_inspect_parameters
2121
from .core import (
2222
DEFAULT_MARKUP_MODE,
@@ -1498,7 +1498,6 @@ def get_param(param: ParamMeta) -> TyperArgument | TyperOption:
14981498
annotation=annotation,
14991499
parameter_info=parameter_info,
15001500
)
1501-
runtime_param = build_runtime_param(descriptor)
15021501
tuple_nargs = descriptor.tuple_arity
15031502

15041503
if isinstance(parameter_info, OptionInfo):
@@ -1547,7 +1546,6 @@ def get_param(param: ParamMeta) -> TyperArgument | TyperOption:
15471546
nargs=tuple_nargs,
15481547
# Rich settings
15491548
rich_help_panel=parameter_info.rich_help_panel,
1550-
runtime_param=runtime_param,
15511549
type_descriptor=descriptor,
15521550
)
15531551
elif isinstance(parameter_info, ArgumentInfo):
@@ -1581,7 +1579,6 @@ def get_param(param: ParamMeta) -> TyperArgument | TyperOption:
15811579
max=parameter_info.max,
15821580
# Rich settings
15831581
rich_help_panel=parameter_info.rich_help_panel,
1584-
runtime_param=runtime_param,
15851582
type_descriptor=descriptor,
15861583
)
15871584
raise AssertionError("A Parameter should be returned") # pragma: no cover

0 commit comments

Comments
 (0)