-
-
Notifications
You must be signed in to change notification settings - Fork 157
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
Apply Policy parameter processing/validation logic to Behavior class #1952
Conversation
….r.t. other parameters" This reverts commit 18f3e4f.
I forgot to start on a clean branch. Thus, the first commit "Adds ability to use operator when validating w.r.t. other parameters" is an experiment I did a couple weeks ago and is unrelated to this PR. |
taxcalc/behavior.json
Outdated
"range": {"min": false, "max": true}, | ||
"out_of_range_minmsg": "", | ||
"out_of_range_maxmsg": "", | ||
"out_of_range_action": "stop" | ||
}, |
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.
Should this parameter be boolean?
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.
Which parameter are you talking about?
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.
Sorry about that. I'm referring to _BE_subinc_wrt_earnings
.
taxcalc/behavior.py
Outdated
last_reform_year = max(reform_years) | ||
if last_reform_year > self.end_year: | ||
msg = 'ERROR: {} YEAR reform provision in YEAR > end_year={}' | ||
raise ValueError(msg.format(last_reform_year, self.end_year)) |
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.
All of the year validation logic may be overkill for the behavior class. However, I thought it would be interesting to show how much of the Policy
code could be copied over.
taxcalc/behavior.py
Outdated
################### | ||
if not self._ignore_errors and self.behavior_errors: | ||
raise ValueError(self.behavior_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.
Policy
doesn't check whether there are error messages after doing value validation in _validate_parameter_values
. Why doesn't Policy
do this?
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.
Checking is the responsibility of the calling script.
See, for example, the basic recipe in the Cookbook.
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 see. Thanks.
taxcalc/behavior.py
Outdated
'ERROR: ' + msg.format(year, name) + '\n' | ||
) | ||
######################## | ||
|
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 won't be used by the Behavior
class, but again, I thought it would be interesting to show how much code from the Policy
class could simply be copied over.
taxcalc/behavior.py
Outdated
'ERROR: ' + | ||
msg.format(year, pname, pvalue[idx]) + | ||
'\n' | ||
) |
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 test case:
# test_behavior.py
def test_validate_param_names_types_errors():
...
+ behv1 = Behavior()
+ specs1 = {2019: {'_BE_inc': [True]}} # _BE_inc should be a float (int_type = boolean_type = False)
+ with pytest.raises(ValueError):
+ behv1.update_behavior(specs1)
did not throw a ValueError
before the second part of the conditional statement was added. See policy.py for original conditional statement.
@hdoupe said:
Thanks for the bug report, @hdoupe. Can you provide detail in a new issue? |
All tests are passing except for the test coverage checks. There are three spots that are no longer hit by the test suite. It looks like there is not a path to them now that the |
taxcalc/parameters.py
Outdated
@@ -94,9 +94,6 @@ def set_default_vals(self, known_years=999999): | |||
""" | |||
if hasattr(self, '_vals'): | |||
for name, data in self._vals.items(): | |||
if not isinstance(name, six.string_types): | |||
msg = 'parameter name {} is not a string' | |||
raise ValueError(msg.format(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.
Paths to this section of code:
Policy.__init__
--> ParametersBase.initialize
--> Parameters.set_default_vals
Behavior.__init__
--> ParametersBase.initialize
--> Parameters.set_default_vals
In both of these cases, the values are read directly from the defaults specified in current_law_policy.json
and behavior.json
. Thus, it is impossible to have an incorrect name unless the name specified in the defaults files is a number or something like that. A test could potentially be added for this possibility.
Policy.implement_reform
-->
self._validate_parameter_names_types(reform)
....
# optionally apply cpi_offset to inflation_rates and re-initialize
if Policy._cpi_offset_in_reform(reform):
known_years = self._apply_reform_cpi_offset(reform)
self.set_default_vals(known_years=known_years)
--> ParametersBase.set_default_vals
However, Policy._validate_parameter_names_types
was called directly before set_default_vals
. Thus, we can be certain that the names are strings and valid otherwise.
taxcalc/parameters.py
Outdated
msg = 'parameter name {} not in parameter values dictionary' | ||
raise ValueError(msg.format(name)) | ||
vals_indexed = self._vals[name].get('cpi_inflated', False) | ||
integer_values = self._vals[name].get('integer_value') | ||
name_plus_cpi = name + '_cpi' |
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.
Paths to this section of code:
Policy.implement_reform
--> ParametersBase._update
Behavior.update_behavior
--> ParametersBase._update
These are both called after calling the Policy and Behavior classes' respective _validate_param_names_types
methods. Thus, we can be certain that the name is in self._vals
. Also, I think an equivalent condition pname not in self._vals
is already in Policy._validate_param_names_types
lines 464-468 and was added to Behavior._validate_param_names_types
.
@@ -387,9 +380,6 @@ def _update(self, year_mods): | |||
for name in unused_names: | |||
used_names.add(name) | |||
pname = name[:-4] # root parameter name | |||
if pname not in self._vals: | |||
msg = 'root parameter name {} not in values dictionary' | |||
raise ValueError(msg.format(pname)) | |||
pindexed = year_mods[year][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.
Paths to this section of code:
Policy.implement_reform
--> ParametersBase._update
Policy._validate_param_names_values
is called before ParametersBase._update
. Policy._validate_param_names_values
has a similar check:
pname = name[:-4] # root parameter name
if pname not in param_names:
msg = '{} {} unknown parameter name'
self.reform_errors += (
'ERROR: ' + msg.format(year, name) + '\n'
)
I said:
After further consideration, it looks like these lines of code could still be hit by @martinholmer what do you think about this? |
@hdoupe said:
I agree. Now that the parameters are being input only by reading files, it seems as there is no need for this kind of run-time checking. However, we do need to be sure that the unit tests include checks of the content of the JSON files, so that a developer does not mistakenly specify invalid data in the JSON files. Do we now have that kind of testing of the content of the |
@hdoupe said:
Actually, I think the constructor for the Growfactors class does not have a dictionary argument. Does it make sense to proceed this way? |
@martinholmer asked:
I do not see any testing that does this. So we should probably add it. I can go ahead and add this. We just want to read in the defaults files and make sure that the keys are all strings, correct? |
@martinholmer said:
Yep, this is correct. Thanks for pointing this out.
Sounds sensible to me. |
@hdoupe proposed this change to the
We don't want to drop these three lines, do we? If a JSON reform file includes a parameter named Unless I'm confused, we want to keep these three lines and add a test that raises a ValueError when the reform includes something like |
@hdoupe said:
You could easily add the test that each key is a string to this
|
@martinholmer said:
So, you are saying that a JSON reform file with an incorrect CPI parameter name needs this validation? Something like:
should trigger those lines of code? |
@hdoupe asked:
Right. |
@martinholmer I tried the following test:
And, I get a However, I may be missing something. |
@hdoupe said:
OK, I see. You were correct from the beginning. The Policy class implement_reform method catches this error before the ParametersBase _update method is called. Sorry, about the confusion. The way you had it before seems correct. |
Ok, thanks for thoroughly reviewing that change. This is a complex process and it's easy for either one (or both) of us to miss something. |
@hdoupe, I notice one of the improvements I've made based on your efforts is causing conflicts in the
Then I'll be happy to merge #1952. Thanks again for all the great work. |
@martinholmer thanks! And thanks for thoroughly reviewing these changes and walking me through this part of Tax-Calculator. I enjoyed learning more about the parameter processing logic in Tax-Calculator. I think I have a much better grasp about how it works now. Also, a second set of eyes to review complex code such as this makes a big difference. |
This PR tries to resolve #1951 by applying the
Policy
parameter validation logic to theBehavior
class. Fortunately, the validation code in thePolicy
"just worked" in theBehavior
class. The attributes and methods that were copied over are:implement_reform
method was implemented asupdate_behavior
_validate_parameter_names_types
boolean_type
andint_type
asfalse
. It turns outisinstance(True, (int, float))
isTrue
. I'm not sure if this was desired behavior or notBehavior
class. So, it could be removed._validate_parameter_values
current_law_version
asbaseline_version
reform_errors
asbehavior_errors
reform_warnings
asbehavior_warnings
_ignore_errors
The
behaviors.json
file had to be updated so that each parameter had the range and out_of_range messages and action. Please double check my work. I'm not very familiar with these parameters. Also, thebehaviors.json
document indicates that_BE_subinc_wrt_earnings
is a boolean parameter, but the current validation logic seems to indicate that it could be a float or boolean or integer:The
tbi.reform_warnings_errors
function was updated to validate thebehavior
parameters in theuser_mods
dictionary. This function now returns a dictionary that looks like:I'll make the changes for this part on the PolicyBrain side.
I have a few more questions that I will ask in code comments.
@martinholmer I'm interested in your thoughts on this PR. If you agree with this approach, what do you think about refactoring the
_validate_parameter_names_types
and_validate_parameter_values
methods into theParametersBase
class? If we do that, perhaps, it wouldn't be too hard to apply it to the other classes such asGrowFactors
.