-
-
Notifications
You must be signed in to change notification settings - Fork 129
Forbid extra fields when structuring dictionaries into attrs classes #142
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
Conversation
Codecov Report
@@ Coverage Diff @@
## master #142 +/- ##
==========================================
+ Coverage 98.21% 98.22% +0.01%
==========================================
Files 6 6
Lines 615 620 +5
==========================================
+ Hits 604 609 +5
Misses 11 11
Continue to review full report at Codecov.
|
src/cattr/gen.py
Outdated
| ) | ||
| lines.append(" }") | ||
| if forbid_extra_fields: | ||
| post_lines.append(" allowed_fields = {") |
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.
Wasn't sure how much of a (non-)goal it is to have tidy generated code. For example, I could probably have written
post_lines.append(f" allowed_fields = {set(a.name for a in attrs)}")which is shorter and maybe a little cleaner from the point of view of this function, but would likely lead to a very long line in the generated code in many cases.
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.
Hello,
thanks for your work on this, let's see about tidying this up and getting it into the next release.
I would also possibly add a parameter for the GenConverter, similar to omit_if_default, that would be used for generating all classes here. (At this level, it'd be OK for the flag to be called just forbid_extra_keys.)
src/cattr/gen.py
Outdated
|
|
||
| def make_dict_structure_fn(cl: Type, converter, **kwargs): | ||
| def make_dict_structure_fn( | ||
| cl: Type, converter, forbid_extra_fields: bool = False, **kwargs |
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.
The argument at this layer should be a little obfuscated, since it might shadow an attribute name you're trying to override (in **kwargs). I'd change it to _cattrs_forbid_extra_fields. Note that we can add this flag on the GenConverter level with a better name.
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.
Ah, I wondered about that. Will do.
src/cattr/gen.py
Outdated
| " }", | ||
| " unknown_fields = set(o.keys()) - allowed_fields", | ||
| " if unknown_fields:", | ||
| " raise KeyError(", |
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 wouldn't use a KeyError here. First of all, KeyError is raised when a key is missing from a dictionary, not when it's extra :)
Second, KeyError is a built-in Python exception. User libraries are supposed either throw Exception or something inheriting from Exception (see https://docs.python.org/3/library/exceptions.html#Exception). Ideally we would have a StructureError and you could inherit from that, but we don't currently and that should be a different task, so just throw Exception for now.
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.
Hm, I think KeyError counts as "something inheriting from Exception", but agreed that custom exception subclasses would be best, and that a KeyError is a little weird.
try:
raise KeyError('key error')
except Exception:
print("caught your key error")
# prints caught your key errorI was thinking of it as the key being missing from the class, rather than from the dictionary. But I'm happy to put in Exception for now if that's your preference!
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.
Yeah, let's switch to a bare Exception for now. KeyError is a bit of a stretch I think.
|
While working on this round of updates, I noticed that you'd suggested Started thinking about tests a little tonight, but would probably appreciate some feedback on what sort of tests are desired here:
|
|
PS. Thanks for the quick turnaround with a review last night! |
Er yeah, keys is better.
If you don't have Hypothesis experience it's ok to skip. Yeah, I'd just add a test or two to
That would be great, yeah. You can add a test to Thanks! |
|
Added some tests. Ended up feeling lazy tonight about tests for Started on docs, but not done there. |
|
Cool, no rush. Thanks! |
|
Okay, I think I'd be ready to call this done. Let me know if there are other changes you'd like to see. |
|
Also: I'm accustomed to a merge/squash workflow to keep git history tidy, but have been assuming it would be appropriate to keep the history as-is to make reviewing easy. LMK if/when it's appropriate to squash. |
Tinche
left a comment
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.
Great job! Left a comment inline.
Also, to save me from opening up a PR to your PR, can you just apply this change:
if _cattrs_forbid_extra_keys:
allowed_fields = {a.name for a in attrs}
globs["__c_a"] = allowed_fields
post_lines += [
" unknown_fields = set(o.keys()) - __c_a",
" if unknown_fields:",
" raise Exception(",
f" 'Extra fields in constructor for {cl_name}: ' + ', '.join(unknown_fields)"
" )",
]
(i.e. precalculate allowed_fields and make it available in the globals).
tests/metadata/test_genconverter.py
Outdated
|
|
||
|
|
||
| @given(simple_typed_attrs(defaults=True), unstructure_strats) | ||
| def test_simple_roundtrip_defaults_with_extra_keys_forbidden( |
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.
Hm, not sure what this test is supposed to cover that the test before (test_simple_roundtrip_with_extra_keys_forbidden) doesn't? If nothing important, feel free to remove (Hypothesis tests take some time to run, would like to keep their number down).
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 think what I had in mind at the time was to make sure that it hadn't somehow made it error in the wrong order (i.e. when there are keys present in the structured object that aren't present in the dict), but given the current implementation I think that's not really a concern, and test_forbid_extra_keys_nested_override would probably catch that in a simple case anyway, so I've gone ahead and deleted. Definitely nice to avoid having too many tests with hypothesis - can really increase the testing time.
|
Should be set I think. Pre-calculating the allowed fields is a nice touch - thanks for that pointer. |
|
I don't really have a policy for squashing, feel free if you feel like it. I will take another look at this in the next day or two and maybe merge it it. Note that GitHub's telling me this branch cannot be rebased to due conflicts for some reason |
Default is to maintain the old behavior. - Add `_cattr_forbid_extra_keys` arg to `make_dict_structure_fn` and implement new behavior - Update `GenConverter` with `forbid_extra_keys` option to enable this for all classes - Add tests and update docs
5982028 to
fea3292
Compare
|
Squashed. |
|
Great works, thanks a lot! |
Would resolve #101. I'm assuming this will need some further work (e.g. new tests; fixing other tests this breaks, etc), but wanted to go ahead and get it up for a first review to see if the approach is reasonable.