-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
to_tap_class
, and inspect fields instead of signature for data models
#128
Conversation
Hi @kddubey, The PR looks great so far! Thank you for all the work you put in! @swansonk14 and I will meet to discuss this PR within a week. My opinion is that both of the advantages that you listed of We'll keep you posted. Thank you again! --Jesse |
Hi @kddubey, We are in support of two different functions Here's a version that we were working on https://github.com/swansonk14/typed-argument-parser/blob/convert_to_tap/tap/tapify.py, but we haven't had time to complete it. Any more contributions in this direction would be greatly appreciated! Thank you for what you have done already! We'll revisit this PR in the next few weeks and find a way to get this into main! Best, |
@martinjm97 thank you for the feedback about Remaining todos for me before I un-draft this PR and ask for a full review:
|
Thank you so much again! Wow! |
Very sorry! @swansonk14 and I are just clearing out time to give this a proper code review. This is our number 1 priority on Tap until it's in. Thank you so much for your tremendous effort and extremely high quality work. --Jesse |
No worries, take your time :-) |
Hi @kddubey, Thank you for all the tremendous work you have put into this PR! We learned a lot from your use of pytest fixtures/parameterize and appreciated the organization of We read over all of your code and it looks great! However, when started testing we found a small bug. When we Dataclasses have the desired functionality: from dataclasses import dataclass
from tap import Tap, tapify
@dataclass
class Model:
id: int = 1
args = tapify(Model)
print(args.id)
from tap import Tap, tapify
from pydantic import BaseModel
class Model(BaseModel):
id: int = 1
args = tapify(Model)
print(args.id)
We still want to spend more time testing your implementation before merging it into main. Thank you so much again for all of this fantastic work! We plan to merge this in as soon as we've finished testing and (you or we) have fixed issues that arise. Best, |
@martinjm97 see this comment and this comment. Note that, by default, a Pydantic |
Sorry for missing your comments! We'll be sure to read all the comments before reporting supposed bugs! Thanks, |
No worries. That comment from a week ago wasn't really clear now that I'm reading it. And I appreciate that you're testing the code independently! |
Hi @martinjm97, the last commit changes the Pydantic implementation to not allow extra arguments for Pydantic I also added support for passing extra command line args to the pydantic model if the model is configured that way. This change is backwards compatible, i.e., you can already do this in main. For example, for this script— # demo_extra_args.py
from pydantic import BaseModel, ConfigDict
from tap import tapify
class User(BaseModel):
model_config = ConfigDict(extra="allow") # explicitly allow extra attributes
name: str
if __name__ == "__main__":
user = tapify(User)
print(user) —running
prints
(Extra args get cast to string, which matches what happens in main. The user can add custom validation if they want to change this behavior.) Removing the
Lmk what you think. Thanks! |
unflake flaky test add some comments
Hi @kddubey, Thank you for all of the extensive testing and bearing with us! @swansonk14 and I went through all of the examples in https://docs.pydantic.dev/latest/concepts/fields/ and did our own testing. Everything that we could think of seems to be working. Thank you again for all your hard work and excellent execution! We will merge it in. We plan to do another pass over code to make small changes to the code style and comments before including it in the next release. Best, |
Sounds good, and feel free to lmk if you need any help or clarification on this code in the future :-) |
Thank you! It has been a pleasure to learn from your work and to work with you. Sorry for the slow turn around. Kyle and I are so happy this landed! --Jesse |
I'd love to see a new release that includes this change; looking forward to using Pydantic models with |
@tinkerware |
We've been trying to find time to get it at least above the codecov threshold before making the new release. Sorry for the delay! |
@martinjm97 Happy to help w/ that since I'm not as busy. And cuz I broke it lol :-) I believe the root cause is that the coverage % is based on a coverage report generated by tests ran in an evironment which doesn't have |
@kddubey, that would be absolutely spectacular if possible. No worries if it doesn't work out though. Thank you again! |
@tinkerware, thanks to the work of @kddubey, the implementation was prepared for release. You can see it here: https://github.com/swansonk14/typed-argument-parser/releases/tag/v_1.10.0. --Jesse |
Great, now I can ditch the git-ref in my poetry dependencies list! I needed the changes in this PR so that I could use Thanks again to the OP and the maintainers for all the work and effort you put into this project! |
So, I wanted to write up a quick summary of how I'm using Pydantic and this project together to parse CLI flags, so it can be a pointer for others looking to do the same and for me to learn from comments. I use Pydantic to parse configuration and to represent the directives of the CLI flags with a "command" abstraction: from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
class AppConfig(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file='.env',
env_file_encoding='utf-8',
env_ignore_empty=True,
str_strip_whitespace=True,
revalidate_instances='always',
frozen=True
)
api_key: str | None = None
debug: bool = False
...
ShowOption: TypeAlias = Literal[...]
ShowInput: TypeAlias = Literal['all', ShowOption]
class AppCommand(BaseModel):
"""Makes hard things easy in a pretty way."""
show: list[ShowInput] = Field(
default=["sensible-default"],
description="..."
)
...
@field_validator("show")
@classmethod
def validate_show(cls, show: list[ShowInput]) -> list[ShowOption]:
if "all" in show:
return [...] # can expand convenience flags into underlying options
return show
def parse_flags(*flags: str) -> tuple[AppConfig, AppCommand]:
class AppFlags(to_tap_class(AppConfig), to_tap_class(AppCommand)):
def configure() -> None:
self.description = AppCommand.__doc__
args = AppFlags().parse_args(args=flags or None).as_dict()
return env_config(**args), AppCommand(**args) This lets me override any configuration using CLI flags, and allows to use Pydantic validators to convert CLI flags to the data that the rest of the app expects. Configuration overrides from CLI flags look something like this (e.g. the try:
_ENV_CONFIG = AppConfig()
except ValidationError as e:
fail("Ensure all `APP_*` environment variables are set correctly: {}", e)
def env_config(**overrides: Any) -> AppConfig:
if not overrides:
return _ENV_CONFIG
# The actual changes are overrides that are different from default values.
changes = (
_ENV_CONFIG
.model_copy(update=overrides)
.model_dump(exclude_defaults=True)
)
return AppConfig.model_validate(
_ENV_CONFIG.model_copy(update=changes)
) This lets me pass a I hope this makes sense and gives you an idea of how others can make use of better integration with Pydantic. |
Wow, very cool to see that level of sophistication @tinkerware. Thank you for sharing this example and explaining it! |
@swansonk14 minor note: should the |
I've updated the release notes with the example from @tinkerware as well as a discussion of the --Jesse |
(Updated summary of changes)
This is a WIP following up on #125. It should also close #112.
2 advantages to using
tap_class_from_data_model
instead oftapify
:-h
help stringTap
class, which allows the user to overrideconfigure
andprocess_args
. See the example here.Future refactor
I'm thinking we can allow
tapify
to return aTap
class instead of calling the input object.@martinjm97 What do you think of this refactor? Lmk if I should open a new issue for this, or a PR if that makes things more concrete.
For now, my plan is to focus on the todos above. The downside to merging in
tap_class_from_data_model
is that there would be 2 interfaces—tapify
andtap_class_from_data_model
—which "tapify" a Pydantic model or dataclass, when there could be 1:tapify
.