First Check
Example Code# example.py
from typing import Annotated
import typer
app = typer.Typer()
class MyCustomType:
def __init__(self, value: str):
try:
self.first, self.second = value.split("/")
except ValueError:
raise typer.BadParameter(f"{value} is not in a valid first/second format") from None
def parse_my_custom_type(value: str) -> MyCustomType:
return MyCustomType(value)
@app.command()
def my_command(arg: Annotated[MyCustomType, typer.Argument(parser=parse_my_custom_type)]) -> None:
typer.echo(f"First: {arg.first}, Second: {arg.second}")
if __name__ == "__main__":
app()DescriptionNote The example code requires Custom types originally showed in the help as the name of the type returned by the parsing function (or perhaps it was the name of the Recently, this changed such that the arg/option displays the name of the parsing function instead. Help text generally is about communicating what type of data/values are allowed and what the default value is. For the case of a custom type, it feels like the contract is something like "the supplied value must either be able to be parsed into a valid instance of this custom type, or else it's a bad parameter error." With the parsing function name being shown, it feels more like Typer is showing the implementation details underneath that contract instead. Operating SystemmacOS Operating System DetailsNo response Project Version0.27.0 Python Version3.14.5 Additional ContextNo response |
Replies: 2 comments 8 replies
|
Thanks for the report, that doesn't feel quite right. I'll investigate! |
|
I bisected this against your example. The name shown has been the parser function all along. What changed in 0.27.0 is only how it is rendered, and the new rendering is what makes it read as an implementation detail. Python 3.14.6, click held constant at 8.4.2 for every row: So the boundary is 0.26.8 to 0.27.0, and The change is the single breaking item in the 0.27.0 notes, "Update metavar printing", PR #1863, in if metavar is None:
- metavar = self.type.name.upper()
+ type_name = self.type.name
+ if type_name.startswith("<") and type_name.endswith(">"):
+ metavar = type_name
+ else:
+ metavar = f"<{type_name}>"
elif parameter_info.parser is not None:
return types.FuncParamType(parameter_info.parser)and Your point about the contract still stands, and I think it is sharper than it first looks: Two things I checked that do not help
Inspecting the built command confirms it: What does work todayBoth verified on 0.27.1: from typer._click.types import ParamType
class MyCustomParamType(ParamType):
name = "mycustomtype"
def convert(self, value, param, ctx):
try:
return MyCustomType(value)
except ValueError:
self.fail(f"{value} is not in a valid first/second format", param, ctx)
# arg <mycustomtype> [required]or, staying on public API only, renaming the parser: parse_my_custom_type.__name__ = "mycustomtype"
# arg <mycustomtype> [required]Neither is really a solution. The first reaches into a private module and typer exports no public |
Upon looking further, I see that this could partially be a "me" problem, but is maybe still something that could be improved somehow: the
parseargument can either take a function or a class that meets the protocol of having aparsemethod. In some places I was providing the class as theparseargument, and this displayed<TheClass>(orTHECLASSin the past) as expected, whereas in others I was providingTheClass.parseas the argument, which unexpectedly (to me) displayed as<parse>.It makes sense now why this is the case, and I suspect it isn't a regression that was introduced (as noted elsewhere in the discussion). I wonder though if there's an opportunity to lean into principle of lea…