diff --git a/taxcalc/cli/tc.py b/taxcalc/cli/tc.py index 5293c3da4..3577e152e 100644 --- a/taxcalc/cli/tc.py +++ b/taxcalc/cli/tc.py @@ -6,10 +6,9 @@ # pep8 --ignore=E402 tc.py # pylint --disable=locally-disabled tc.py -import os import sys import argparse -from taxcalc import TaxCalcIO, Policy +from taxcalc import TaxCalcIO def main(): @@ -86,56 +85,27 @@ def main(): default=False, action="store_true") args = parser.parse_args() - arg_errors = False - # check INPUT file name - if args.INPUT == '': - sys.stderr.write('ERROR: must specify INPUT file name\n') - arg_errors = True - else: - if not args.INPUT.endswith('.csv'): - sys.stderr.write('ERROR: INPUT file name does not end in .csv\n') - arg_errors = True - elif not os.path.isfile(args.INPUT): - sys.stderr.write('ERROR: INPUT file could not be found\n') - arg_errors = True - # check TAXYEAR value - first_taxyear = Policy.JSON_START_YEAR - last_taxyear = Policy.LAST_BUDGET_YEAR - if args.TAXYEAR < first_taxyear: - sys.stderr.write('ERROR: TAXYEAR < {}\n'.format(first_taxyear)) - arg_errors = True - elif args.TAXYEAR > last_taxyear: - sys.stderr.write('ERROR: TAXYEAR > {}\n'.format(last_taxyear)) - arg_errors = True - # check REFORM value - if args.reform is not None: - if not args.reform.endswith('.json'): - sys.stderr.write('ERROR: REFORM file name does not end in .json\n') - arg_errors = True - elif not os.path.isfile(args.reform): - sys.stderr.write('ERROR: REFORM file could not be found\n') - arg_errors = True - # check ASSUMP value - if args.assump is not None: - if not args.assump.endswith('.json'): - sys.stderr.write('ERROR: ASSUMP file name does not end in .json\n') - arg_errors = True - elif not os.path.isfile(args.assump): - sys.stderr.write('ERROR: ASSUMP file could not be found\n') - arg_errors = True - # exit if any argument errors - if arg_errors: - sys.stderr.write('USAGE: tc --help\n') - return 1 # instantiate TaxCalcIO object and do tax analysis - aging = args.INPUT.endswith('puf.csv') or args.INPUT.endswith('cps.csv') tcio = TaxCalcIO(input_data=args.INPUT, tax_year=args.TAXYEAR, reform=args.reform, - assump=args.assump, - growdiff_response=None, - aging_input_data=aging, - exact_calculations=args.exact) + assump=args.assump) + if len(tcio.errmsg) > 0: + sys.stderr.write(tcio.errmsg) + sys.stderr.write('USAGE: tc --help\n') + return 1 + aging = args.INPUT.endswith('puf.csv') or args.INPUT.endswith('cps.csv') + tcio.init(input_data=args.INPUT, + tax_year=args.TAXYEAR, + reform=args.reform, + assump=args.assump, + growdiff_response=None, + aging_input_data=aging, + exact_calculations=args.exact) + if len(tcio.errmsg) > 0: + sys.stderr.write(tcio.errmsg) + sys.stderr.write('USAGE: tc --help\n') + return 1 tcio.analyze(writing_output_file=True, output_tables=args.tables, output_graphs=args.graphs, diff --git a/taxcalc/taxcalcio.py b/taxcalc/taxcalcio.py index 53f7ce5fa..8f0f12f64 100644 --- a/taxcalc/taxcalcio.py +++ b/taxcalc/taxcalcio.py @@ -49,50 +49,22 @@ class TaxCalcIO(object): None implies economic assumptions are standard assumptions, or string is name of optional ASSUMP file. - growdiff_response: Growdiff object or None - growdiff_response Growdiff object is used only by the - TaxCalcIO.growmodel_analysis method; must be None in all other cases. - - aging_input_data: boolean - whether or not to extrapolate Records data from data year to tax_year. - - exact_calculations: boolean - specifies whether or not exact tax calculations are done without - any smoothing of "stair-step" provisions in the tax law. - - Raises - ------ - ValueError: - if input_data is neither string nor pandas DataFrame. - if input_data string does not have .csv extension. - if file specified by input_data string does not exist. - if reform is neither None nor string. - if reform string does not have .json extension. - if file specified by reform string does not exist. - if assump is neither None nor string. - if assump string does not have .json extension. - if growdiff_response is not a Growdiff object or None - if file specified by assump string does not exist. - if tax_year before Policy start_year. - if tax_year after Policy end_year. - if growdiff_response in --assump ASSUMP has any response. - Returns ------- class instance: TaxCalcIO """ - def __init__(self, input_data, tax_year, reform, assump, - growdiff_response, - aging_input_data, exact_calculations): + def __init__(self, input_data, tax_year, reform, assump): """ - TaxCalcIO class constructor. + TaxCalcIO class constructor, which must be followed by init() call. """ # pylint: disable=too-many-arguments # pylint: disable=too-many-locals # pylint: disable=too-many-branches # pylint: disable=too-many-statements - # check for existence of INPUT file + self.errmsg = '' + # check name and existence of INPUT file + inp = 'x' if isinstance(input_data, six.string_types): # remove any leading directory path from INPUT filename fname = os.path.basename(input_data) @@ -100,34 +72,41 @@ def __init__(self, input_data, tax_year, reform, assump, if fname.endswith('.csv'): inp = '{}-{}'.format(fname[:-4], str(tax_year)[2:]) else: - msg = 'INPUT file named {} does not end in .csv' - raise ValueError(msg.format(fname)) + msg = 'INPUT file name does not end in .csv' + self.errmsg += 'ERROR: {}\n'.format(msg) # check existence of INPUT file if not os.path.isfile(input_data): - msg = 'INPUT file named {} could not be found' - raise ValueError(msg.format(input_data)) + msg = 'INPUT file could not be found' + self.errmsg += 'ERROR: {}\n'.format(msg) elif isinstance(input_data, pd.DataFrame): inp = 'df-{}'.format(str(tax_year)[2:]) else: msg = 'INPUT is neither string nor Pandas DataFrame' - raise ValueError(msg) - # construct output_filename and delete old output files if they exist + self.errmsg += 'ERROR: {}\n'.format(msg) + # check name and existence of REFORM file + ref = '-x' if reform is None: - specified_reform = False + self.specified_reform = False ref = '-#' elif isinstance(reform, six.string_types): - specified_reform = True + self.specified_reform = True # remove any leading directory path from REFORM filename fname = os.path.basename(reform) # check if fname ends with ".json" if fname.endswith('.json'): ref = '-{}'.format(fname[:-5]) else: - msg = 'REFORM file named {} does not end in .json' - raise ValueError(msg.format(fname)) + msg = 'REFORM file name does not end in .json' + self.errmsg += 'ERROR: {}\n'.format(msg) + # check existence of REFORM file + if not os.path.isfile(reform): + msg = 'REFORM file could not be found' + self.errmsg += 'ERROR: {}\n'.format(msg) else: msg = 'TaxCalcIO.ctor: reform is neither None nor str' - raise ValueError(msg) + self.errmsg += 'ERROR: {}\n'.format(msg) + # check name and existence of ASSUMP file + asm = '-x' if assump is None: asm = '-#' elif isinstance(assump, six.string_types): @@ -137,28 +116,67 @@ def __init__(self, input_data, tax_year, reform, assump, if fname.endswith('.json'): asm = '-{}'.format(fname[:-5]) else: - msg = 'ASSUMP file named {} does not end in .json' - raise ValueError(msg.format(fname)) + msg = 'ASSUMP file name does not end in .json' + self.errmsg += 'ERROR: {}\n'.format(msg) + # check existence of ASSUMP file + if not os.path.isfile(assump): + msg = 'ASSUMP file could not be found' + self.errmsg += 'ERROR: {}\n'.format(msg) else: msg = 'TaxCalcIO.ctor: assump is neither None nor str' - raise ValueError(msg) + self.errmsg += 'ERROR: {}\n'.format(msg) + # create OUTPUT file name and delete any existing output file self._output_filename = '{}{}{}.csv'.format(inp, ref, asm) delete_file(self._output_filename) delete_file(self._output_filename.replace('.csv', '-tab.text')) delete_file(self._output_filename.replace('.csv', '-atr.html')) delete_file(self._output_filename.replace('.csv', '-mtr.html')) + # initialize variables whose values are set in init method + self.behavior_has_any_response = False + self.calc = None + self.calc_clp = None + + def init(self, input_data, tax_year, reform, assump, + growdiff_response, + aging_input_data, exact_calculations): + """ + TaxCalcIO class post-constructor method that completes initialization. + + Parameters + ---------- + First four parameters are same as for TaxCalcIO constructor: + input_data, tax_year, reform, assump. + + growdiff_response: Growdiff object or None + growdiff_response Growdiff object is used only by the + TaxCalcIO.growmodel_analysis method; + must be None in all other cases. + + aging_input_data: boolean + whether or not to extrapolate Records data from data year to + tax_year. + + exact_calculations: boolean + specifies whether or not exact tax calculations are done without + any smoothing of "stair-step" provisions in the tax law. + """ + # pylint: disable=too-many-arguments + # pylint: disable=too-many-locals + # pylint: disable=too-many-branches + # pylint: disable=too-many-statements + self.errmsg = '' # get parameter dictionaries from --reform and --assump files param_dict = Calculator.read_json_param_files(reform, assump) # create Behavior object beh = Behavior() beh.update_behavior(param_dict['behavior']) - self._behavior_has_any_response = beh.has_any_response() + self.behavior_has_any_response = beh.has_any_response() # make sure no growdiff_response is specified in --assump gdiff_response = Growdiff() gdiff_response.update_growdiff(param_dict['growdiff_response']) if gdiff_response.has_any_response(): - msg = '--assump ASSUMP cannot assume any "growdiff_response"' - raise ValueError(msg) + msg = 'ASSUMP file cannot specify any "growdiff_response"' + self.errmsg += 'ERROR: {}\n'.format(msg) # create gdiff_baseline object gdiff_baseline = Growdiff() gdiff_baseline.update_growdiff(param_dict['growdiff_baseline']) @@ -170,18 +188,20 @@ def __init__(self, input_data, tax_year, reform, assump, gdiff_response = Growdiff() elif isinstance(growdiff_response, Growdiff): gdiff_response = growdiff_response - if self._behavior_has_any_response: - msg = 'cannot assume any "behavior" when using GrowModel' - raise ValueError(msg) + if self.behavior_has_any_response: + msg = 'ASSUMP file cannot specify any "behavior" ' + msg += 'when using GrowModel' + self.errmsg += 'ERROR: {}\n'.format(msg) else: - msg = 'TaxCalcIO.ctor: growdiff_response is neither None nor {}' - raise ValueError(msg.format('a Growdiff object')) + msg = 'TaxCalcIO.more_init: growdiff_response is neither None ' + msg += 'nor a Growdiff object' + self.errmsg += 'ERROR: {}\n'.format(msg) # create Growfactors ref object that has both gdiff objects applied gfactors_ref = Growfactors() gdiff_baseline.apply_to(gfactors_ref) gdiff_response.apply_to(gfactors_ref) # create Policy objects - if specified_reform: + if self.specified_reform: pol = Policy(gfactors=gfactors_ref) pol.implement_reform(param_dict['policy']) else: @@ -190,10 +210,14 @@ def __init__(self, input_data, tax_year, reform, assump, # check for valid tax_year value if tax_year < pol.start_year: msg = 'tax_year {} less than policy.start_year {}' - raise ValueError(msg.format(tax_year, pol.start_year)) + msg = msg.format(tax_year, pol.start_year) + self.errmsg += 'ERROR: {}\n'.format(msg) if tax_year > pol.end_year: msg = 'tax_year {} greater than policy.end_year {}' - raise ValueError(msg.format(tax_year, pol.end_year)) + msg = msg.format(tax_year, pol.end_year) + self.errmsg += 'ERROR: {}\n'.format(msg) + if len(self.errmsg) > 0: + return # invalid tax_year value would cause Policy.set_year error # set policy to tax_year pol.set_year(tax_year) clp.set_year(tax_year) @@ -216,21 +240,21 @@ def __init__(self, input_data, tax_year, reform, assump, # create Calculator objects con = Consumption() con.update_consumption(param_dict['consumption']) - self._calc = Calculator(policy=pol, records=recs, - verbose=True, - consumption=con, - behavior=beh, - sync_years=aging_input_data) - self._calc_clp = Calculator(policy=clp, records=recs_clp, - verbose=False, - consumption=con, - sync_years=aging_input_data) + self.calc = Calculator(policy=pol, records=recs, + verbose=True, + consumption=con, + behavior=beh, + sync_years=aging_input_data) + self.calc_clp = Calculator(policy=clp, records=recs_clp, + verbose=False, + consumption=con, + sync_years=aging_input_data) def tax_year(self): """ Returns year for which TaxCalcIO calculations are being done. """ - return self._calc.policy.current_year + return self.calc.policy.current_year def output_filepath(self): """ @@ -274,25 +298,25 @@ def analyze(self, writing_output_file=False, # pylint: disable=too-many-arguments,too-many-locals,too-many-branches if output_dump: (mtr_paytax, mtr_inctax, - _) = self._calc.mtr(wrt_full_compensation=False) + _) = self.calc.mtr(wrt_full_compensation=False) else: # do not need marginal tax rates mtr_paytax = None mtr_inctax = None - if self._behavior_has_any_response: - self._calc = Behavior.response(self._calc_clp, self._calc) + if self.behavior_has_any_response: + self.calc = Behavior.response(self.calc_clp, self.calc) else: - self._calc.calc_all() + self.calc.calc_all() # optionally conduct normative welfare analysis if output_ceeu: - if self._behavior_has_any_response: + if self.behavior_has_any_response: ceeu_results = 'SKIP --ceeu output because baseline and ' ceeu_results += 'reform cannot be sensibly compared\n ' ceeu_results += ' ' ceeu_results += 'when specifying "behavior" with --assump ' ceeu_results += 'option.' else: - self._calc_clp.calc_all() - cedict = ce_aftertax_income(self._calc_clp, self._calc, + self.calc_clp.calc_all() + cedict = ce_aftertax_income(self.calc_clp, self.calc, require_no_agg_tax_change=False) ceeu_results = TaxCalcIO.ceeu_output(cedict) else: @@ -318,7 +342,7 @@ def write_output_file(self, output_dump, mtr_paytax, mtr_inctax): outdf = self.dump_output(mtr_inctax, mtr_paytax) else: outdf = self.minimal_output() - assert len(outdf.index) == self._calc.records.dim + assert len(outdf.index) == self.calc.records.dim outdf.to_csv(self._output_filename, index=False, float_format='%.2f') def write_tables_file(self): @@ -330,7 +354,7 @@ def write_tables_file(self): # create expanded-income decile table containing weighted total levels record_cols = ['s006', '_payrolltax', '_iitax', 'lumpsum_tax', '_combined', '_expanded_income'] - out = [getattr(self._calc.records, col) for col in record_cols] + out = [getattr(self.calc.records, col) for col in record_cols] dfx = pd.DataFrame(data=np.column_stack(out), columns=record_cols) # skip tables if there are not some positive weights if dfx['s006'].sum() <= 0: @@ -377,12 +401,12 @@ def write_graph_files(self): """ Write graphs to HTML files. """ - atr_data = atr_graph_data(self._calc_clp, self._calc) + atr_data = atr_graph_data(self.calc_clp, self.calc) atr_plot = xtr_graph_plot(atr_data) atr_fname = self._output_filename.replace('.csv', '-atr.html') atr_title = 'ATR by Income Percentile' write_graph_file(atr_plot, atr_fname, atr_title) - mtr_data = mtr_graph_data(self._calc_clp, self._calc, + mtr_data = mtr_graph_data(self.calc_clp, self.calc, alt_e00200p_text='Taxpayer Earnings') mtr_plot = xtr_graph_plot(mtr_data) mtr_fname = self._output_filename.replace('.csv', '-mtr.html') @@ -395,7 +419,7 @@ def minimal_output(self): """ varlist = ['RECID', 'YEAR', 'WEIGHT', 'INCTAX', 'LSTAX', 'PAYTAX'] odict = dict() - crecs = self._calc.records + crecs = self.calc.records odict['RECID'] = crecs.RECID # id for tax filing unit odict['YEAR'] = self.tax_year() # tax calculation year odict['WEIGHT'] = crecs.s006 # sample weight @@ -454,7 +478,7 @@ def dump_output(self, mtr_inctax, mtr_paytax): odf = pd.DataFrame() varset = Records.USABLE_READ_VARS | Records.CALCULATED_VARS for varname in varset: - vardata = getattr(self._calc.records, varname) + vardata = getattr(self.calc.records, varname) odf[varname] = vardata odf['FLPDYR'] = self.tax_year() # tax calculation year odf['mtr_inctax'] = mtr_inctax @@ -475,10 +499,10 @@ def growmodel_analysis(input_data, tax_year, reform, assump, Parameters ---------- First six parameters are same as the first six parameters of - the TaxCalcIO constructor. + the TaxCalcIO.init method. Last five parameters are same as the first five parameters of - the TaxCalcIO analyze method. + the TaxCalcIO.analyze method. Returns ------- @@ -520,10 +544,10 @@ def annual_analysis(input_data, tax_year, reform, assump, Parameters ---------- First six parameters are same as the first six parameters of - the TaxCalcIO constructor. + the TaxCalcIO.init method. Last five parameters are same as the first five parameters of - the TaxCalcIO analyze method. + the TaxCalcIO.analyze method. Returns ------- @@ -534,10 +558,14 @@ def annual_analysis(input_data, tax_year, reform, assump, tcio = TaxCalcIO(input_data=input_data, tax_year=year, reform=reform, - assump=assump, - growdiff_response=growdiff_response, - aging_input_data=aging_input_data, - exact_calculations=exact_calculations) + assump=assump) + tcio.init(input_data=input_data, + tax_year=year, + reform=reform, + assump=assump, + growdiff_response=growdiff_response, + aging_input_data=aging_input_data, + exact_calculations=exact_calculations) if year == tax_year: # conduct final tax analysis for year equal to tax_year tcio.analyze(writing_output_file=writing_output_file, diff --git a/taxcalc/tests/test_taxcalcio.py b/taxcalc/tests/test_taxcalcio.py index d7ec52acc..76700e465 100644 --- a/taxcalc/tests/test_taxcalcio.py +++ b/taxcalc/tests/test_taxcalcio.py @@ -7,21 +7,24 @@ import os import tempfile -from io import StringIO import pytest import pandas as pd # pylint: disable=import-error from taxcalc import TaxCalcIO, Growdiff -RAWINPUTFILE_FUNITS = 4 -RAWINPUTFILE_CONTENTS = ( - u'RECID,MARS\n' - u' 1, 2\n' - u' 2, 1\n' - u' 3, 4\n' - u' 4, 6\n' -) +@pytest.mark.parametrize("input_data, reform, assump", [ + ('no-dot-csv-filename', 'no-dot-json-filename', 'no-dot-json-filename'), + (list(), list(), list()), + ('no-exist.csv', 'no-exist.json', 'no-exist.json'), +]) +def test_ctor_errors(input_data, reform, assump): + """ + Ensure error messages are generated by TaxCalcIO.__init__. + """ + tcio = TaxCalcIO(input_data=input_data, tax_year=2013, + reform=reform, assump=assump) + assert len(tcio.errmsg) > 0 @pytest.yield_fixture @@ -30,7 +33,14 @@ def rawinputfile(): Temporary input file that contains minimum required input variables. """ ifile = tempfile.NamedTemporaryFile(suffix='.csv', mode='a', delete=False) - ifile.write(RAWINPUTFILE_CONTENTS) + contents = ( + u'RECID,MARS\n' + u' 1, 2\n' + u' 2, 1\n' + u' 3, 4\n' + u' 4, 6\n' + ) + ifile.write(contents) ifile.close() # must close and then yield for Windows platform yield ifile @@ -41,37 +51,17 @@ def rawinputfile(): pass # sometimes we can't remove a generated temporary file -@pytest.mark.parametrize("input_data, exact", [ - ('no-dot-csv-filename', True), - (list(), False), - ('bad_filename.csv', False), -]) -def test_incorrect_creation_1(input_data, exact): - """ - Ensure a ValueError is raised when created with invalid data pointers. - """ - with pytest.raises(ValueError): - TaxCalcIO(input_data=input_data, - tax_year=2013, - reform=None, - assump=None, - growdiff_response=None, - aging_input_data=False, - exact_calculations=exact) - - @pytest.yield_fixture def reformfile0(): """ Specify JSON reform file. """ txt = """ - { - "policy": { - "_SS_Earnings_c": {"2016": [300000], - "2018": [500000], - "2020": [700000]} - } + { "policy": { + "_SS_Earnings_c": {"2016": [300000], + "2018": [500000], + "2020": [700000]} + } } """ rfile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) @@ -86,36 +76,62 @@ def reformfile0(): pass # sometimes we can't remove a generated temporary file +@pytest.yield_fixture +def assumpfile0(): + """ + Temporary assumption file with .json extension. + """ + afile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) + contents = """ + { + "consumption": {}, + "behavior": {"_BE_sub": {"2020": [0.05]}}, + "growdiff_baseline": {}, + "growdiff_response": {"_ABOOK": {"2015": [-0.01]}} + } + """ + afile.write(contents) + afile.close() + # must close and then yield for Windows platform + yield afile + if os.path.isfile(afile.name): + try: + os.remove(afile.name) + except OSError: + pass # sometimes we can't remove a generated temporary file + + # for fixture args, pylint: disable=redefined-outer-name -@pytest.mark.parametrize("year, ref, asm, gdr", [ - (2013, list(), None, None), - (2013, None, list(), None), - (2001, None, None, None), - (2099, None, None, None), - (2020, 'no-dot-json-reformfile', None, None), - (2020, 'reformfile0', 'no-dot-json-assumpfile', None), - (2020, 'reformfile0', None, dict()), +@pytest.mark.parametrize("year, asm, gdr", [ + (2000, 'assumpfile0', Growdiff()), + (2099, None, None), + (2020, None, dict()), ]) -def test_incorrect_creation_2(rawinputfile, reformfile0, year, ref, asm, gdr): +def test_init_errors(reformfile0, assumpfile0, year, asm, gdr): """ - Ensure a ValueError is raised when created with invalid parameters. + Ensure error messages generated by TaxCalcIO.init method. """ - # pylint: disable=too-many-arguments - if ref == 'reformfile0': - reform = reformfile0.name + recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} + recdf = pd.DataFrame(data=recdict, index=[0]) + if asm == 'assumpfile0': + assump = assumpfile0.name else: - reform = ref - with pytest.raises(ValueError): - TaxCalcIO( - input_data=rawinputfile.name, - tax_year=year, - reform=reform, - assump=asm, - growdiff_response=gdr, - aging_input_data=False, - exact_calculations=False) + assump = asm + tcio = TaxCalcIO(input_data=recdf, + tax_year=year, + reform=reformfile0.name, + assump=assump) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=recdf, + tax_year=year, + reform=reformfile0.name, + assump=assump, + growdiff_response=gdr, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) > 0 def test_creation_with_aging(rawinputfile, reformfile0): @@ -126,70 +142,68 @@ def test_creation_with_aging(rawinputfile, reformfile0): tcio = TaxCalcIO(input_data=rawinputfile.name, tax_year=taxyear, reform=reformfile0.name, - assump=None, - growdiff_response=Growdiff(), - aging_input_data=True, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=rawinputfile.name, + tax_year=taxyear, + reform=reformfile0.name, + assump=None, + growdiff_response=Growdiff(), + aging_input_data=True, + exact_calculations=False) + assert len(tcio.errmsg) == 0 assert tcio.tax_year() == taxyear taxyear = 2016 tcio = TaxCalcIO(input_data=rawinputfile.name, tax_year=taxyear, reform=None, - assump=None, - growdiff_response=None, - aging_input_data=True, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=rawinputfile.name, + tax_year=taxyear, + reform=None, + assump=None, + growdiff_response=None, + aging_input_data=True, + exact_calculations=False) + assert len(tcio.errmsg) == 0 assert tcio.tax_year() == taxyear -REFORM1_CONTENTS = """ -// Example of a reform file suitable for the read_json_param_files() function. -// This JSON file can contain any number of trailing //-style comments, which -// will be removed before the contents are converted from JSON to a dictionary. -// Within the "policy" object, the primary keys are parameters and -// secondary keys are years. -// Both the primary and secondary key values must be enclosed in quotes ("). -// Boolean variables are specified as true or false (no quotes; all lowercase). -{ - "policy": { - "_AMT_brk1": // top of first AMT tax bracket - {"2015": [200000], - "2017": [300000] - }, - "_EITC_c": // maximum EITC amount by number of qualifying kids (0,1,2,3+) - {"2016": [[ 900, 5000, 8000, 9000]], - "2019": [[1200, 7000, 10000, 12000]] - }, - "_II_em": // personal exemption amount (see indexing changes below) - {"2016": [6000], - "2018": [7500], - "2020": [9000] - }, - "_II_em_cpi": // personal exemption amount indexing status - {"2016": false, // values in future years are same as this year value - "2018": true // values in future years indexed with this year as base - }, - "_SS_Earnings_c": // social security (OASDI) maximum taxable earnings - {"2016": [300000], - "2018": [500000], - "2020": [700000] - }, - "_AMT_em_cpi": // AMT exemption amount indexing status - {"2017": false, // values in future years are same as this year value - "2020": true // values in future years indexed with this year as base - } - } -} -""" - - @pytest.yield_fixture def reformfile1(): """ Temporary reform file with .json extension. """ rfile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) - rfile.write(REFORM1_CONTENTS) + contents = """ + { "policy": { + "_AMT_brk1": { // top of first AMT tax bracket + "2015": [200000], + "2017": [300000]}, + "_EITC_c": { // max EITC amount by number of qualifying kids (0,1,2,3+) + "2016": [[ 900, 5000, 8000, 9000]], + "2019": [[1200, 7000, 10000, 12000]]}, + "_II_em": { // personal exemption amount (see indexing changes below) + "2016": [6000], + "2018": [7500], + "2020": [9000]}, + "_II_em_cpi": { // personal exemption amount indexing status + "2016": false, // values in future years are same as this year value + "2018": true // values in future years indexed with this year as base + }, + "_SS_Earnings_c": { // social security (OASDI) maximum taxable earnings + "2016": [300000], + "2018": [500000], + "2020": [700000]}, + "_AMT_em_cpi": { // AMT exemption amount indexing status + "2017": false, // values in future years are same as this year value + "2020": true // values in future years indexed with this year as base + } + } + } + """ + rfile.write(contents) rfile.close() # must close and then yield for Windows platform yield rfile @@ -200,34 +214,20 @@ def reformfile1(): pass # sometimes we can't remove a generated temporary file -ASSUMP_CONTENTS = """ -// Example of an assump file suitable for the read_json_param_files() function. -// This JSON file can contain any number of trailing //-style comments, which -// will be removed before the contents are converted from JSON to a dictionary. -// Within each "consumption", "behavior", "growdiff_baseline" and -// "growdiff_response" object, the primary keys are parameters and -// the secondary keys are years. -// Both the primary and secondary key values must be enclosed in quotes ("). -// Boolean variables are specified as true or false (no quotes; all lowercase). -{ - "title": "", - "author": "", - "date": "", - "consumption": { "_MPC_e18400": {"2018": [0.05]} }, - "behavior": {}, - "growdiff_baseline": {}, - "growdiff_response": {} -} -""" - - @pytest.yield_fixture def assumpfile1(): """ Temporary assumption file with .json extension. """ afile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) - afile.write(ASSUMP_CONTENTS) + contents = """ + { "consumption": { "_MPC_e18400": {"2018": [0.05]} }, + "behavior": {}, + "growdiff_baseline": {}, + "growdiff_response": {} + } + """ + afile.write(contents) afile.close() # must close and then yield for Windows platform yield afile @@ -246,10 +246,16 @@ def test_output_otions(rawinputfile, reformfile1, assumpfile1): tcio = TaxCalcIO(input_data=rawinputfile.name, tax_year=taxyear, reform=reformfile1.name, - assump=assumpfile1.name, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=assumpfile1.name) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=rawinputfile.name, + tax_year=taxyear, + reform=reformfile1.name, + assump=assumpfile1.name, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 outfilepath = tcio.output_filepath() # --ceeu output and standard output try: @@ -292,14 +298,20 @@ def test_no_tables(reformfile1): idict['e00300'] = [10000 * i for i in range(1, nobs + 1)] idict['_expanded_income'] = idict['e00300'] idf = pd.DataFrame(idict, columns=list(idict)) - # create TaxCalcIO tables file + # create and initialize TaxCalcIO object tcio = TaxCalcIO(input_data=idf, tax_year=2020, reform=reformfile1.name, - assump=None, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=idf, + tax_year=2020, + reform=reformfile1.name, + assump=None, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 # create TaxCalcIO tables file tcio.analyze(writing_output_file=False, output_tables=True) # delete tables file @@ -322,14 +334,20 @@ def test_tables(reformfile1): idict['e00300'] = [10000 * i for i in range(1, nobs + 1)] idict['_expanded_income'] = idict['e00300'] idf = pd.DataFrame(idict, columns=list(idict)) - # create TaxCalcIO tables file + # create and initialize TaxCalcIO object tcio = TaxCalcIO(input_data=idf, tax_year=2020, reform=reformfile1.name, - assump=None, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=idf, + tax_year=2020, + reform=reformfile1.name, + assump=None, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 # create TaxCalcIO tables file tcio.analyze(writing_output_file=False, output_tables=True) # delete tables file @@ -352,14 +370,20 @@ def test_graphs(reformfile1): idict['e00300'] = [10000 * i for i in range(1, nobs + 1)] idict['_expanded_income'] = idict['e00300'] idf = pd.DataFrame(idict, columns=list(idict)) - # create TaxCalcIO graph files + # create and initialize TaxCalcIO object tcio = TaxCalcIO(input_data=idf, tax_year=2020, reform=reformfile1.name, - assump=None, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=idf, + tax_year=2020, + reform=reformfile1.name, + assump=None, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 tcio.analyze(writing_output_file=False, output_graphs=True) # delete graph files output_filename = tcio.output_filepath() @@ -379,7 +403,7 @@ def lumpsumreformfile(): rfile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) lumpsum_reform_contents = """ { - "policy": {"_LST": {"2013": [200]}} + "policy": {"_LST": {"2013": [200]}} } """ rfile.write(lumpsum_reform_contents) @@ -403,10 +427,16 @@ def test_ceeu_output(lumpsumreformfile): tcio = TaxCalcIO(input_data=recdf, tax_year=taxyear, reform=lumpsumreformfile.name, - assump=None, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=None) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=recdf, + tax_year=taxyear, + reform=lumpsumreformfile.name, + assump=None, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 tcio.analyze(writing_output_file=False, output_ceeu=True) assert tcio.tax_year() == taxyear @@ -446,73 +476,20 @@ def test_ceeu_with_behavior(lumpsumreformfile, assumpfile2): tcio = TaxCalcIO(input_data=recdf, tax_year=taxyear, reform=lumpsumreformfile.name, - assump=assumpfile2.name, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) + assump=assumpfile2.name) + assert len(tcio.errmsg) == 0 + tcio.init(input_data=recdf, + tax_year=taxyear, + reform=lumpsumreformfile.name, + assump=assumpfile2.name, + growdiff_response=None, + aging_input_data=False, + exact_calculations=False) + assert len(tcio.errmsg) == 0 tcio.analyze(writing_output_file=False, output_ceeu=True) assert tcio.tax_year() == taxyear -def test_bad_ctor_with_growmodel(lumpsumreformfile, assumpfile2): - """ - Test improper TaxCalcIO constructor calls when using GrowModel analysis. - """ - taxyear = 2020 - recdict = {'RECID': 1, 'MARS': 1, 'e00300': 100000, 's006': 1e8} - recdf = pd.DataFrame(data=recdict, index=[0]) - with pytest.raises(ValueError): - TaxCalcIO(input_data=recdf, - tax_year=taxyear, - reform=lumpsumreformfile.name, - assump=assumpfile2.name, - growdiff_response=Growdiff(), - aging_input_data=False, - exact_calculations=False) - - -@pytest.yield_fixture -def assumpfile_bad1(): - """ - Temporary assumption file with .json extension. - """ - afile = tempfile.NamedTemporaryFile(suffix='.json', mode='a', delete=False) - bad1_assump_contents = """ - { - "consumption": {}, - "behavior": {}, - "growdiff_baseline": {}, - "growdiff_response": {"_ABOOK": {"2015": [-0.01]}} - } - """ - afile.write(bad1_assump_contents) - afile.close() - # must close and then yield for Windows platform - yield afile - if os.path.isfile(afile.name): - try: - os.remove(afile.name) - except OSError: - pass # sometimes we can't remove a generated temporary file - - -def test_bad_assumption_file(reformfile1, assumpfile_bad1): - """ - Test TaxCalcIO constructor with illegal assumptions. - """ - input_stream = StringIO(RAWINPUTFILE_CONTENTS) - input_dataframe = pd.read_csv(input_stream) - taxyear = 2022 - with pytest.raises(ValueError): - TaxCalcIO(input_data=input_dataframe, - tax_year=taxyear, - reform=reformfile1.name, - assump=assumpfile_bad1.name, - growdiff_response=None, - aging_input_data=False, - exact_calculations=False) - - def test_growmodel_analysis(reformfile1, assumpfile1): """ Test TaxCalcIO.growmodel_analysis method with no output.