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
feat: use custom root type for KeyBinding #65
Conversation
Codecov Report
@@ Coverage Diff @@
## main #65 +/- ##
==========================================
- Coverage 99.34% 99.30% -0.05%
==========================================
Files 31 31
Lines 1692 1729 +37
==========================================
+ Hits 1681 1717 +36
- Misses 11 12 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't say I love the extra code + complexity here, but I also don't have any better ideas, so approving because the desired behavior is at least well tested.
I had one optional idea around mutability that might simplify things, but not sure if it's viable.
Also, to confirm, the reason we want KeyBinding
serialized to a string (rather than a list of dicts) is because we want it to human-readable and modifiable in something like a JSON or YAML file?
return self._parts | ||
|
||
@no_type_check | ||
def __setattr__(self, key, val): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea: do we need to support mutation of KeyBinding
? This adds extra code to maintain, which is mostly just a repeat of __init__
anyway.
I don't think performance is a serious concern here right? Especially since parts
is likely to remain the only member of this thing anyway.
If we made this model frozen/immutable, then we could just derive the parts
property (likely as a cached property) and maybe add a factory method like KeyBinding.from_parts
to avoid some of the complexity in __init__
too.
@@ -100,6 +117,37 @@ def test_in_dict(): | |||
assert kbs[hash(new_a)] == 0 | |||
|
|||
|
|||
def test_errors(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love the test coverage of error cases.
errors=[ErrorWrapper(exc=exc, loc=("parts",))], model=cls | ||
) | ||
|
||
def __init__(self, **data: Any) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: any strong reason to use **data
instead of expanding the two known parameters __root__
and parts
as named keyword arguments with typing info?
I agree, while I also think the goal is reasonable (not having to pass
I think making this immutable is a great idea. In any case, even if it's not immutable, I dislike the duplicated logic in the
This is also my main question here: while I can see that it's a nicer way to serialize it, can you point out where you ran into this problem? i.e. where you were trying to serialize it (rather than, say, using (an alternative very simple solution here is to set the json encoder on the model that contains the keybinding, so it would be nice to know why/where that approach is insufficient) |
"""Key combinations that make up the overall key chord.""" | ||
return self._parts | ||
|
||
@no_type_check |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please type stuff in this library
if you do want to change the root type of __root__: str
@property
def parts(self) -> List[SimpleKeyBinding]:
# could cache this and make keybinding immutable
return [SimpleKeyBinding.from_str(part) for part in self.__root__.split()] (and then get rid of all the |
You can find the issue I ran into in this Zulip thread Here's a simple example you can run yourself if you're not convinced: In [1]: from pydantic import BaseModel
In [2]: class Client(BaseModel):
...: title: str
...: first: str
...: last: str
...:
In [3]: class Lawyer(BaseModel):
...: client: Client
...:
...: class Config:
...: json_encoders = {
...: Client: lambda c: f'{c.title} {c.first} {c.last}'
...: }
...:
In [4]: class Office(BaseModel):
...: lawyer: Lawyer
...:
...: class Config:
...: json_encoders = {
...: Client: lambda c: f'{c.title} {c.first} {c.last}'
...: }
...:
In [5]: client = Client(title='Dr.', first='Medikal', last='School')
In [6]: client.json(models_as_dict=False)
Out[6]: '{"title": "Dr.", "first": "Medikal", "last": "School"}'
In [7]: lawyer = Lawyer(client=client)
In [8]: lawyer.json(models_as_dict=False)
Out[8]: '{"client": "Dr. Medikal School"}'
In [9]: office = Office(lawyer=lawyer)
In [10]: office.json(models_as_dict=False)
Out[10]: '{"lawyer": {"client": {"title": "Dr.", "first": "Medikal", "last": "School"}}}' |
I don't need convincing of pydantic's behavior :) I'm aware of it/your previous examples were convincing enough I guess, I'm kinda asking where exactly in your code you needed this. I did read the zulip thread and don't see an application there, just more MREs... and a link to the github issue where you said:
but what I'm looking for is the literal place where you're using keybinding, would you mind linking it? (i.e. whats your def enc_client(c: 'Client'):
return f"{c.title} {c.first} {c.last}"
class Office(BaseModel):
lawyer: Lawyer
class Config:
json_encoders = {Client: enc_client, Lawyer: lambda l: {'client': enc_client(l.client)}} |
Ohhhhh, I see what you’re getting at. The equivalent of When I have time I’ll try adding a |
yeah, give it a shot. I do still agree that it's frustrating that you can't have the logic that declares how a field should be json encoded on the field itself... rather than needing to take special action in the higher level models that use the field. Note that napari/napari#2297 was trying to solve this internally for napari (e.g. if you define a |
I made a branch experimenting with the above, but ran into an issue with representing fully-default models. Tried to debug but just more and more issues keep popping up due to our custom handling of YAML among other things. I think at this point it would be simpler to proceed with this PR with some of your requested changes, so that's what I'll do. |
If I understand correctly this would be superseded by #68? |
yes |
closing... but feel free to reopen if I'm mistaken! |
Re-opening this, ran into some more issues where we couldn't generate the schema from Do you think another solution would be better? We could theoretically subclass |
just curious, how did using napari's |
more context about the issues you're running into would also be appreciated. minimal examples using pydantic are helpful, but i know that there are lots of little tricky bits with pydantic, so seeing the actual code and problems you're running into would be very helpful here so we're not just talking in the abstract my gut feeling here is that using |
It worked for
this is the failing test, basically even though we have our own json encoders defined, when it comes to generating the schema, they... just use their default one! :))))))) im at the point where i just want to rewrite a ton of pydantics source code |
ohhh... I see, your issue is in
well you're in luck, they're hard at work on v2, re-written in rust. |
as a side comment here: I also think that using the json-schema autogenerator thing in the napari preferences dialog has been stretched well past its original scope (and usefulness) ... so another thing you could consider here is simply excluding this from the schema there, and creating a custom widget without going through json-schema-form-thingy |
yes, turns out that's called later down the chain after it's already been passed to their default encoding function which throws an error
thank god im actually at my wit's end with this library's design, perhaps it's time to learn rust and contribute 😓 |
yes, no doubt there are frustrations. but i think on the whole, it's an excellent idea and library, with some hiccups in the original implementation. (something I think we should all be able to relate to as napari developers 😂) each time I find myself thinking I could do better, I find it harder than at first I thought |
Use a custom root type so that
models_as_dict=False
does not need to be passed to the.json()
method since this parameter does not get passed recursively along models. This changes the.dict()
method to replaceKeyBinding
with its__root__
, which is astr
, thus effectively having the data represented as a string for serialization purposes while still being able to access the rest of the functionality attached to theKeyBinding
class.If I have implemented this correctly, there should be no effective changes to the API.