-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Strict validation in configuration file (v2 only) #4607
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
Ok, I think this is done! |
readthedocs/config/config.py
Outdated
after the key, if no value is passed, | ||
it raises an exception when the key is not found. | ||
""" | ||
raise_ex = not bool(args) |
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.
So... I don't like much this hack, but I didn't find a better way to handle this. We can't put something like default=None
, because we may need to default some values to None sometimes too. And I didn't want to add another parameter like default=None, raise_ex
.
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.
Why can't the method just be:
def pop_config(self, key, default=None, raise_ex=True):
return self.pop(key.split('.'), self.raw_config, default, raise_ex
This still works if normal usage is:
self.pop_config('formats', [])
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 didn't want to have another param here, but looks better than my hacky solution p:
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.
raise_ex
should be False here by default, so when we call self.pop_config('key', 'default')
we don't need to pass another value like self.pop_config('key', 'default', raise_ex=False)
. But when we don't provide a default value, we need to called it like self.pop_config('key', raise_ex=False)
.
I'm trying to replicate the same behaviour from https://docs.python.org/3/library/stdtypes.html#dict.pop. Looks like the implementation is inside the c code.
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.
It seems like raise_ex=False
would be the default value then. Does than not solve your use case?
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.
Already implemented that in the last commit!
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.
raise_ex = not bool(args)
and the rest of this method still seem like a non-standard way of doing a standard thing to me:
def pop_config(self, key, default=None, raise_ex=False):
if default is None:
raise_ex = True
return self.pop(key.split('.'), self.raw_config, default, raise_ex)
This seems to be the same logic without having to manually handle *args
and hacking raise_ex
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.
Actually, we have more cases where default=None
and shouldn't raise an exception, we only have one use of default=None
and raise_ex=True
. I don't think is a good idea to modify args based on other args.
This is the current implementation
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.
👍 This is a nice way of implementing a check for left over keys. Noted some minor cleanup
readthedocs/config/config.py
Outdated
after the key, if no value is passed, | ||
it raises an exception when the key is not found. | ||
""" | ||
raise_ex = not bool(args) |
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.
Why can't the method just be:
def pop_config(self, key, default=None, raise_ex=True):
return self.pop(key.split('.'), self.raw_config, default, raise_ex
This still works if normal usage is:
self.pop_config('formats', [])
readthedocs/config/config.py
Outdated
if wrong_key: | ||
self.error( | ||
wrong_key, | ||
'Unsuported configuration: {}. Maybe a typo?'.format(wrong_key), |
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.
Typo at Unsuported
.
Also, perhaps Unsupported configuration option: {}
for more specificity.
Maybe a typo?
might not be clear for non-english speakers as well, so maybe something more formal 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.
That was the most formal sentence that I came up 😞
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.
you honestly probably don't need the second portion of the error message. I think "unsupported configuration option: {x}" says enough
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.
Already changed in the last commit
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 this PR works, but I'd like to see some more tests and improve the docstring because I think the flow is confusing when you read the code for the first time.
I'm marking as "Comment" my review though, to not be that strict for these changes.
'build.extra' | ||
) | ||
]) | ||
def test_strict_validation(self, value, key): |
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 may worth to have some unit test for the specific methods that do the trick: _get_extra_key
and validate_keys
.
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.
In particular the one that is recursive with different start/stop conditions.
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.
validate_keys
is already called when running validate
. _get_extra_key
is validated here https://github.com/stsewd/readthedocs.org/blob/strict-validation/readthedocs/config/tests/test_config.py#L1718-L1743
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 have added one more test, but we are already covered anyway with the other tests (they don't throw an exception).
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.
Not throwing an exception doesn't test that _get_extra_key
returns the correct keys. That's the test I wanted.
The same for validate_keys
to check that if there is extra keys, it fails.
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.
actually test_strict_validation
test that, an error is thrown and key
and code
are the correct in the error. We test with validate
as entrypoint, as that's the way to use the config module (all tests were changed to test like that)
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 understand what you say. Although, I'd like to have a unit test for the methods I mentioned before for these cases:
_get_extra_key
- returns no extra keys
- a couple of cases where it returns extra keys
validate_keys
- a couple of cases where it raises
- the same for no raising the exception
The test_strict_validation
is testing only one case and using the complete flow. I'd like to test the inner pieces, in particular the than that it's recursive (it's very easy to make a mistake on initial conditions)
readthedocs/config/config.py
Outdated
""" | ||
Checks that we don't have extra keys. | ||
|
||
This should be called after all the validations are done. |
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.
Why? Can you expand a little more here?
If we know that we have an extra key and the build will fail because of this, why we are going to validate all the values first if we already know that it will fail?
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 I got it now, but it's hard to read.
I suppose that when validating the keys we pop
(remove) these keys from the raw_config
and finally if there are some leftovers it means that we have invalid keys.
If this is the way it works, it would be good to clarify in the docstring.
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.
Yep, that is
readthedocs/config/config.py
Outdated
submodules['recursive'] = validate_bool(recursive) | ||
|
||
return submodules | ||
|
||
def validate_keys(self): | ||
""" | ||
Checks that we don't have extra keys. |
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'd say Checks that we don't have invalid keys.
readthedocs/config/config.py
Outdated
This should be called after all the validations are done. | ||
""" | ||
msg = ( | ||
'Unsupported configuration option: {}. ' |
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'd say Invalid configuration option:
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.
That makes more sense here
'Unsupported configuration option: {}. ' | ||
'Make sure the key name is correct.' | ||
) | ||
self.pop_config('version', 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.
What's this for?
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.
We always pass the version
key, but that's is already validated from outside
Changes look good to me, when @humitos gives the 👍 here, feel free to rebase/fix conflicts and merge! |
aef1b9f
to
d33854a
Compare
readthedocs/config/config.py
Outdated
""" | ||
Get the extra keyname (list form) of a dict object. | ||
|
||
If there are more the one key, the first one is returned. |
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.
"more than one extra key"
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.
Excellent! Thanks for the tests 😍
I left a comment for a typo :)
Close #4455
I only need to add some docs. And maybe refactor a little.